diff options
Diffstat (limited to 'lib')
260 files changed, 13590 insertions, 6990 deletions
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp new file mode 100644 index 00000000..ad7c5f99 --- /dev/null +++ b/lib/accountregistry.cpp @@ -0,0 +1,137 @@ +// 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" +#include <QtCore/QCoreApplication> + +using namespace Quotient; + +void AccountRegistry::add(Connection* a) +{ + if (contains(a)) + return; + beginInsertRows(QModelIndex(), size(), size()); + push_back(a); + endInsertRows(); + emit accountCountChanged(); +} + +void AccountRegistry::drop(Connection* a) +{ + if (const auto idx = indexOf(a); idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + remove(idx); + endRemoveRows(); + } + Q_ASSERT(!contains(a)); +} + +bool AccountRegistry::isLoggedIn(const QString &userId) const +{ + return std::any_of(cbegin(), cend(), [&userId](const Connection* a) { + return a->userId() == userId; + }); +} + +QVariant AccountRegistry::data(const QModelIndex& index, int role) const +{ + if (!index.isValid() || index.row() >= count()) + return {}; + + if (role == AccountRole) + return QVariant::fromValue(at(index.row())); + + return {}; +} + +int AccountRegistry::rowCount(const QModelIndex& parent) const +{ + return parent.isValid() ? 0 : count(); +} + +QHash<int, QByteArray> AccountRegistry::roleNames() const +{ + return { { AccountRole, "connection" } }; +} + +Connection* AccountRegistry::get(const QString& userId) +{ + for (const auto &connection : *this) { + if (connection->userId() == userId) + return connection; + } + return nullptr; +} + +QKeychain::ReadPasswordJob* AccountRegistry::loadAccessTokenFromKeychain(const QString& userId) +{ + qCDebug(MAIN) << "Reading access token from keychain for" << userId; + auto job = new QKeychain::ReadPasswordJob(qAppName(), this); + job->setKey(userId); + job->start(); + + return job; +} + +void AccountRegistry::invokeLogin() +{ + const auto accounts = SettingsGroup("Accounts").childGroups(); + for (const auto& accountId : accounts) { + AccountSettings account { accountId }; + m_accountsLoading += accountId; + emit accountsLoadingChanged(); + + if (account.homeserver().isEmpty()) + continue; + + auto accessTokenLoadingJob = + loadAccessTokenFromKeychain(account.userId()); + connect(accessTokenLoadingJob, &QKeychain::Job::finished, this, + [accountId, this, accessTokenLoadingJob]() { + if (accessTokenLoadingJob->error() + != QKeychain::Error::NoError) { + emit keychainError(accessTokenLoadingJob->error()); + return; + } + + AccountSettings account { accountId }; + auto connection = new Connection(account.homeserver()); + connect(connection, &Connection::connected, this, + [connection, this, accountId] { + connection->loadState(); + connection->setLazyLoading(true); + + connection->syncLoop(); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); + }); + connect(connection, &Connection::loginError, this, + [this, connection, accountId](const QString& error, + const QString& details) { + emit loginError(connection, error, details); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); + }); + connect(connection, &Connection::resolveError, this, + [this, connection, accountId](const QString& error) { + emit resolveError(connection, error); + + m_accountsLoading.removeAll(accountId); + emit accountsLoadingChanged(); + }); + connection->assumeIdentity( + account.userId(), accessTokenLoadingJob->binaryData(), + account.deviceId()); + }); + } +} + +QStringList AccountRegistry::accountsLoading() const +{ + return m_accountsLoading; +} diff --git a/lib/accountregistry.h b/lib/accountregistry.h new file mode 100644 index 00000000..9560688e --- /dev/null +++ b/lib/accountregistry.h @@ -0,0 +1,92 @@ +// 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 "quotient_export.h" +#include "settings.h" + +#include <QtCore/QAbstractListModel> + +#if QT_VERSION_MAJOR >= 6 +# include <qt6keychain/keychain.h> +#else +# include <qt5keychain/keychain.h> +#endif + +namespace QKeychain { +class ReadPasswordJob; +} + +namespace Quotient { +class Connection; + +class QUOTIENT_API AccountRegistry : public QAbstractListModel, + private QVector<Connection*> { + Q_OBJECT + /// Number of accounts that are currently fully loaded + Q_PROPERTY(int accountCount READ rowCount NOTIFY accountCountChanged) + /// List of accounts that are currently in some stage of being loaded (Reading token from keychain, trying to contact server, etc). + /// Can be used to inform the user or to show a login screen if size() == 0 and no accounts are loaded + Q_PROPERTY(QStringList accountsLoading READ accountsLoading NOTIFY accountsLoadingChanged) +public: + using vector_t = QVector<Connection*>; + using const_iterator = vector_t::const_iterator; + using const_reference = vector_t::const_reference; + + enum EventRoles { + AccountRole = Qt::UserRole + 1, + ConnectionRole = AccountRole + }; + + [[deprecated("Use Accounts variable instead")]] // + static AccountRegistry& instance(); + + // Expose most of vector_t's const-API but only provide add() and drop() + // for changing it. In theory other changing operations could be supported + // too; but then boilerplate begin/end*() calls has to be tucked into each + // and this class gives no guarantees on the order of entries, so why care. + + const vector_t& accounts() const { return *this; } + void add(Connection* a); + void drop(Connection* a); + const_iterator begin() const { return vector_t::begin(); } + const_iterator end() const { return vector_t::end(); } + const_reference front() const { return vector_t::front(); } + const_reference back() const { return vector_t::back(); } + bool isLoggedIn(const QString& userId) const; + Connection* get(const QString& userId); + + using vector_t::isEmpty, vector_t::empty; + using vector_t::size, vector_t::count, vector_t::capacity; + using vector_t::cbegin, vector_t::cend, vector_t::contains; + + // QAbstractItemModel interface implementation + + [[nodiscard]] QVariant data(const QModelIndex& index, + int role) const override; + [[nodiscard]] int rowCount( + const QModelIndex& parent = QModelIndex()) const override; + [[nodiscard]] QHash<int, QByteArray> roleNames() const override; + + QStringList accountsLoading() const; + + void invokeLogin(); +Q_SIGNALS: + void accountCountChanged(); + void accountsLoadingChanged(); + + void keychainError(QKeychain::Error error); + void loginError(Connection* connection, QString message, QString details); + void resolveError(Connection* connection, QString error); + +private: + QKeychain::ReadPasswordJob* loadAccessTokenFromKeychain(const QString &userId); + QStringList m_accountsLoading; +}; + +inline QUOTIENT_API AccountRegistry Accounts {}; + +inline AccountRegistry& AccountRegistry::instance() { return Accounts; } +} // namespace Quotient diff --git a/lib/avatar.cpp b/lib/avatar.cpp index c65aa25c..13de99bf 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "avatar.h" @@ -37,9 +22,9 @@ public: explicit Private(QUrl url = {}) : _url(move(url)) {} ~Private() { - if (isJobRunning(_thumbnailRequest)) + if (isJobPending(_thumbnailRequest)) _thumbnailRequest->abandon(); - if (isJobRunning(_uploadRequest)) + if (isJobPending(_uploadRequest)) _uploadRequest->abandon(); } @@ -54,7 +39,7 @@ public: // The below are related to image caching, hence mutable mutable QImage _originalImage; - mutable std::vector<QPair<QSize, QImage>> _scaledImages; + mutable std::vector<std::pair<QSize, QImage>> _scaledImages; mutable QSize _requestedSize; mutable enum { Unknown, Cache, Network, Banned } _imageSource = Unknown; mutable QPointer<MediaThumbnailJob> _thumbnailRequest = nullptr; @@ -62,15 +47,11 @@ public: mutable std::vector<get_callback_t> callbacks; }; -Avatar::Avatar() : d(std::make_unique<Private>()) {} +Avatar::Avatar() + : d(makeImpl<Private>()) +{} -Avatar::Avatar(QUrl url) : d(std::make_unique<Private>(std::move(url))) {} - -Avatar::Avatar(Avatar&&) = default; - -Avatar::~Avatar() = default; - -Avatar& Avatar::operator=(Avatar&&) = default; +Avatar::Avatar(QUrl url) : d(makeImpl<Private>(std::move(url))) {} QImage Avatar::get(Connection* connection, int dimension, get_callback_t callback) const @@ -87,7 +68,7 @@ QImage Avatar::get(Connection* connection, int width, int height, bool Avatar::upload(Connection* connection, const QString& fileName, upload_callback_t callback) const { - if (isJobRunning(d->_uploadRequest)) + if (isJobPending(d->_uploadRequest)) return false; return d->upload(connection->uploadFile(fileName), move(callback)); } @@ -95,7 +76,7 @@ bool Avatar::upload(Connection* connection, const QString& fileName, bool Avatar::upload(Connection* connection, QIODevice* source, upload_callback_t callback) const { - if (isJobRunning(d->_uploadRequest) || !source->isReadable()) + if (isJobPending(d->_uploadRequest) || !source->isReadable()) return false; return d->upload(connection->uploadContent(source), move(callback)); } @@ -125,7 +106,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size, && checkUrl(_url)) { qCDebug(MAIN) << "Getting avatar from" << _url.toString(); _requestedSize = size; - if (isJobRunning(_thumbnailRequest)) + if (isJobPending(_thumbnailRequest)) _thumbnailRequest->abandon(); if (callback) callbacks.emplace_back(move(callback)); @@ -143,9 +124,9 @@ QImage Avatar::Private::get(Connection* connection, QSize size, }); } - for (const auto& p : _scaledImages) - if (p.first == size) - return p.second; + for (const auto& [scaledSize, scaledImage] : _scaledImages) + if (scaledSize == size) + return scaledImage; auto result = _originalImage.isNull() ? QImage() : _originalImage.scaled(size, Qt::KeepAspectRatio, @@ -157,7 +138,7 @@ QImage Avatar::Private::get(Connection* connection, QSize size, bool Avatar::Private::upload(UploadContentJob* job, upload_callback_t &&callback) { _uploadRequest = job; - if (!isJobRunning(_uploadRequest)) + if (!isJobPending(_uploadRequest)) return false; _uploadRequest->connect(_uploadRequest, &BaseJob::success, _uploadRequest, [job, callback] { callback(job->contentUri()); }); @@ -194,7 +175,7 @@ bool Avatar::updateUrl(const QUrl& newUrl) d->_url = newUrl; d->_imageSource = Private::Unknown; - if (isJobRunning(d->_thumbnailRequest)) + if (isJobPending(d->_thumbnailRequest)) d->_thumbnailRequest->abandon(); return true; } diff --git a/lib/avatar.h b/lib/avatar.h index 7a566bfa..c94dc369 100644 --- a/lib/avatar.h +++ b/lib/avatar.h @@ -1,42 +1,25 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "util.h" + #include <QtCore/QUrl> #include <QtGui/QIcon> #include <functional> -#include <memory> namespace Quotient { class Connection; -class Avatar { +class QUOTIENT_API Avatar { public: explicit Avatar(); explicit Avatar(QUrl url); - Avatar(Avatar&&); - ~Avatar(); - Avatar& operator=(Avatar&&); using get_callback_t = std::function<void()>; - using upload_callback_t = std::function<void(QString)>; + using upload_callback_t = std::function<void(QUrl)>; QImage get(Connection* connection, int dimension, get_callback_t callback) const; @@ -54,8 +37,6 @@ public: private: class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient;
\ No newline at end of file diff --git a/lib/connection.cpp b/lib/connection.cpp index 853053bd..4547474a 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -1,55 +1,55 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2019 Ville Ranki <ville.ranki@iki.fi> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "connection.h" +#include "accountregistry.h" #include "connectiondata.h" -#ifdef Quotient_E2EE_ENABLED -# include "encryptionmanager.h" -#endif // Quotient_E2EE_ENABLED +#include "qt_connection_util.h" #include "room.h" #include "settings.h" #include "user.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" #include "csapi/joining.h" #include "csapi/leaving.h" #include "csapi/logout.h" -#include "csapi/receipts.h" #include "csapi/room_send.h" #include "csapi/to_device.h" -#include "csapi/versions.h" #include "csapi/voip.h" #include "csapi/wellknown.h" +#include "csapi/whoami.h" #include "events/directchatevent.h" -#include "events/eventloader.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" #include "jobs/syncjob.h" +#include <variant> #ifdef Quotient_E2EE_ENABLED -# include "account.h" // QtOlm +# include "database.h" +# include "keyverificationsession.h" + +# include "e2ee/qolmaccount.h" +# include "e2ee/qolminboundsession.h" +# include "e2ee/qolmsession.h" +# include "e2ee/qolmutility.h" +# include "e2ee/qolmutils.h" + +# include "events/keyverificationevent.h" #endif // Quotient_E2EE_ENABLED -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -# include <QtCore/QCborValue> +#if QT_VERSION_MAJOR >= 6 +# include <qt6keychain/keychain.h> +#else +# include <qt5keychain/keychain.h> #endif #include <QtCore/QCoreApplication> @@ -66,7 +66,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();) { @@ -84,8 +84,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; @@ -93,12 +91,11 @@ public: // state is Invited. The spec mandates to keep Invited room state // separately; specifically, we should keep objects for Invite and // Leave state of the same room if the two happen to co-exist. - QHash<QPair<QString, bool>, Room*> roomMap; + QHash<std::pair<QString, bool>, Room*> roomMap; /// Mapping from serverparts to alias/room id mappings, /// as of the last sync QHash<QString, QString> roomAliasMap; QVector<QString> roomIdsToForget; - QVector<Room*> firstTimeRooms; QVector<QString> pendingStateRoomIds; QMap<QString, User*> userMap; DirectChatsMap directChats; @@ -111,13 +108,34 @@ public: QMetaObject::Connection syncLoopConnection {}; int syncTimeout = -1; +#ifdef Quotient_E2EE_ENABLED + QSet<QString> trackedUsers; + QSet<QString> outdatedUsers; + QHash<QString, QHash<QString, DeviceKeys>> deviceKeys; + QueryKeysJob *currentQueryKeysJob = nullptr; + bool encryptionUpdateRequired = false; + PicklingMode picklingMode = Unencrypted {}; + Database *database = nullptr; + QHash<QString, int> oneTimeKeysCount; + std::vector<std::unique_ptr<EncryptedEvent>> pendingEncryptedEvents; + void handleEncryptedToDeviceEvent(const EncryptedEvent& event); + bool processIfVerificationEvent(const Event &evt, bool encrypted); + + // A map from SenderKey to vector of InboundSession + UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions; + + QHash<QString, KeyVerificationSession*> verificationSessions; +#endif + GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; QVector<GetLoginFlowsJob::LoginFlow> loginFlows; #ifdef Quotient_E2EE_ENABLED - QScopedPointer<EncryptionManager> encryptionManager; + std::unique_ptr<QOlmAccount> olmAccount; + bool isUploadingKeys = false; + bool firstSync = true; #endif // Quotient_E2EE_ENABLED QPointer<GetWellknownJob> resolverJob = nullptr; @@ -133,11 +151,6 @@ public: != "json"; bool lazyLoading = false; - /// \brief Stop resolving and login flows jobs, and clear login flows - /// - /// Prepares the class to set or resolve a new homeserver - void clearResolvingContext(); - /** \brief Check the homeserver and resolve it if needed, before connecting * * A single entry for functions that need to check whether the homeserver @@ -155,25 +168,17 @@ public: */ void checkAndConnect(const QString &userId, const std::function<void ()> &connectFn, - const std::optional<LoginFlows::LoginFlow> &flow = none); + const std::optional<LoginFlow> &flow = none); template <typename... LoginArgTs> void loginToServer(LoginArgTs&&... loginArgs); - void completeSetup(const QString& mxId); + void completeSetup(const QString &mxId); void removeRoom(const QString& roomId); void consumeRoomData(SyncDataList&& roomDataList, bool fromCache); void consumeAccountData(Events&& accountDataEvents); void consumePresenceData(Events&& presenceData); void consumeToDeviceEvents(Events&& toDeviceEvents); - - template <typename EventT> - EventT* unpackAccountData() const - { - const auto& eventIt = accountData.find(EventT::matrixTypeId()); - return eventIt == accountData.end() - ? nullptr - : weakPtrCast<EventT>(eventIt->second); - } + void consumeDevicesList(DevicesList&& devicesList); void packAndSendAccountData(EventPtr&& event) { @@ -184,7 +189,7 @@ public: emit q->accountDataChanged(eventType); } - template <typename EventT, typename ContentT> + template <EventClass EventT, typename ContentT> void packAndSendAccountData(ContentT&& content) { packAndSendAccountData( @@ -195,36 +200,115 @@ public: return q->stateCacheDir().filePath("state.json"); } - EventPtr sessionDecryptMessage(const EncryptedEvent& encryptedEvent) +#ifdef Quotient_E2EE_ENABLED + void loadSessions() { + olmSessions = q->database()->loadOlmSessions(picklingMode); + } + void saveSession(const QOlmSession& session, const QString& senderKey) const + { + q->database()->saveOlmSession(senderKey, session.sessionId(), + session.pickle(picklingMode), + QDateTime::currentDateTime()); + } + + template <typename FnT> + std::pair<QString, QString> doDecryptMessage(const QOlmSession& session, + const QOlmMessage& message, + FnT&& andThen) const + { + const auto expectedMessage = session.decrypt(message); + if (expectedMessage) { + const auto result = + std::make_pair(*expectedMessage, session.sessionId()); + andThen(); + return result; + } + const auto errorLine = message.type() == QOlmMessage::PreKey + ? "Failed to decrypt prekey message:" + : "Failed to decrypt message:"; + qCDebug(E2EE) << errorLine << expectedMessage.error(); + return {}; + } + + std::pair<QString, QString> sessionDecryptMessage( + const QJsonObject& personalCipherObject, const QByteArray& senderKey) + { + const auto msgType = static_cast<QOlmMessage::Type>( + personalCipherObject.value(TypeKeyL).toInt(-1)); + if (msgType != QOlmMessage::General && msgType != QOlmMessage::PreKey) { + qCWarning(E2EE) << "Olm message has incorrect type" << msgType; + return {}; + } + QOlmMessage message { + personalCipherObject.value(BodyKeyL).toString().toLatin1(), msgType + }; + for (const auto& session : olmSessions[senderKey]) + if (msgType == QOlmMessage::General + || session->matchesInboundSessionFrom(senderKey, message)) { + return doDecryptMessage(*session, message, [this, &session] { + q->database()->setOlmSessionLastReceived( + session->sessionId(), QDateTime::currentDateTime()); + }); + } + + if (msgType == QOlmMessage::General) { + qCWarning(E2EE) << "Failed to decrypt message"; + return {}; + } + + qCDebug(E2EE) << "Creating new inbound session"; // Pre-key messages only + auto newSessionResult = + olmAccount->createInboundSessionFrom(senderKey, message); + if (!newSessionResult) { + qCWarning(E2EE) + << "Failed to create inbound session for" << senderKey + << "with error" << newSessionResult.error(); + return {}; + } + auto newSession = std::move(*newSessionResult); + if (olmAccount->removeOneTimeKeys(*newSession) != OLM_SUCCESS) { + qWarning(E2EE) << "Failed to remove one time key for session" + << newSession->sessionId(); + // Keep going though + } + return doDecryptMessage( + *newSession, message, [this, &senderKey, &newSession] { + saveSession(*newSession, senderKey); + olmSessions[senderKey].push_back(std::move(newSession)); + }); + } +#endif + + std::pair<EventPtr, QString> sessionDecryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; -#else // Quotient_E2EE_ENABLED +#else if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey) return {}; - const auto identityKey = - encryptionManager->account()->curve25519IdentityKey(); + const auto identityKey = olmAccount->identityKeys().curve25519; const auto personalCipherObject = encryptedEvent.ciphertext(identityKey); if (personalCipherObject.isEmpty()) { qCDebug(E2EE) << "Encrypted event is not for the current device"; return {}; } - const auto decrypted = encryptionManager->sessionDecryptMessage( - personalCipherObject, encryptedEvent.senderKey().toLatin1()); + const auto [decrypted, olmSessionId] = + sessionDecryptMessage(personalCipherObject, + encryptedEvent.senderKey().toLatin1()); if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" << encryptedEvent.senderKey() - << encryptionManager->account()->oneTimeKeys(); + << olmAccount->oneTimeKeys().keys; return {}; } auto&& decryptedEvent = fromJson<EventPtr>(QJsonDocument::fromJson(decrypted.toUtf8())); - if (auto sender = decryptedEvent->fullJson()["sender"_ls].toString(); + if (auto sender = decryptedEvent->fullJson()[SenderKeyL].toString(); sender != encryptedEvent.senderId()) { qCWarning(E2EE) << "Found user" << sender << "instead of sender" << encryptedEvent.senderId() @@ -232,36 +316,97 @@ public: return {}; } + auto query = database->prepareQuery(QStringLiteral("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;")); + query.bindValue(":curveKey", encryptedEvent.contentJson()["sender_key"].toString()); + database->execute(query); + if (!query.next()) { + qCWarning(E2EE) << "Received olm message from unknown device" << encryptedEvent.contentJson()["sender_key"].toString(); + return {}; + } + auto edKey = decryptedEvent->fullJson()["keys"]["ed25519"].toString(); + if (edKey.isEmpty() || query.value(QStringLiteral("edKey")).toString() != edKey) { + qCDebug(E2EE) << "Received olm message with invalid ed key"; + return {}; + } + // TODO: keys to constants const auto decryptedEventObject = decryptedEvent->fullJson(); - const auto recipient = - decryptedEventObject.value("recipient"_ls).toString(); + const auto recipient = decryptedEventObject.value("recipient"_ls).toString(); if (recipient != data->userId()) { qCDebug(E2EE) << "Found user" << recipient << "instead of us" << data->userId() << "in Olm plaintext"; return {}; } - const auto ourKey = - decryptedEventObject.value("recipient_keys"_ls).toObject() - .value(Ed25519Key).toString(); - if (ourKey - != QString::fromUtf8( - encryptionManager->account()->ed25519IdentityKey())) { + const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject() + .value(Ed25519Key).toString(); + if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) { qCDebug(E2EE) << "Found key" << ourKey << "instead of ours own ed25519 key" - << encryptionManager->account()->ed25519IdentityKey() + << olmAccount->identityKeys().ed25519 << "in Olm plaintext"; return {}; } - return std::move(decryptedEvent); + return { std::move(decryptedEvent), olmSessionId }; #endif // Quotient_E2EE_ENABLED } +#ifdef Quotient_E2EE_ENABLED + bool isKnownCurveKey(const QString& userId, const QString& curveKey) const; + + void loadOutdatedUserDevices(); + void saveDevicesList(); + void loadDevicesList(); + + // This function assumes that an olm session with (user, device) exists + std::pair<QOlmMessage::Type, QByteArray> olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const; + bool createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const OneTimeKeys &oneTimeKeyObject); + QString curveKeyForUserDevice(const QString& userId, + const QString& device) const; + QJsonObject assembleEncryptedContent(QJsonObject payloadJson, + const QString& targetUserId, + const QString& targetDeviceId) const; +#endif + + void saveAccessTokenToKeychain() const + { + qCDebug(MAIN) << "Saving access token to keychain for" << q->userId(); + auto job = new QKeychain::WritePasswordJob(qAppName()); + job->setAutoDelete(true); + job->setKey(q->userId()); + job->setBinaryData(data->accessToken()); + job->start(); + //TODO error handling + } + + void dropAccessToken() + { + qCDebug(MAIN) << "Removing access token from keychain for" << q->userId(); + auto job = new QKeychain::DeletePasswordJob(qAppName()); + job->setAutoDelete(true); + job->setKey(q->userId()); + job->start(); + + auto pickleJob = new QKeychain::DeletePasswordJob(qAppName()); + pickleJob->setAutoDelete(true); + pickleJob->setKey(q->userId() + "-Pickle"_ls); + pickleJob->start(); + //TODO error handling + + data->setToken({}); + } }; Connection::Connection(const QUrl& server, QObject* parent) - : QObject(parent), d(new Private(std::make_unique<ConnectionData>(server))) + : QObject(parent) + , d(makeImpl<Private>(std::make_unique<ConnectionData>(server))) { +#ifdef Quotient_E2EE_ENABLED + //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount); +#endif d->q = this; // All d initialization should occur before this line } @@ -271,11 +416,13 @@ Connection::~Connection() { qCDebug(MAIN) << "deconstructing connection object for" << userId(); stopSync(); + Accounts.drop(this); } void Connection::resolveServer(const QString& mxid) { - d->clearResolvingContext(); + if (isJobPending(d->resolverJob)) + d->resolverJob->abandon(); auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid)); maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" @@ -298,7 +445,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"; @@ -326,12 +473,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")); - }); }); } @@ -353,7 +494,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); @@ -380,8 +521,18 @@ void Connection::assumeIdentity(const QString& mxId, const QString& accessToken, { d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] { d->data->setToken(accessToken.toLatin1()); - d->data->setDeviceId(deviceId); - d->completeSetup(mxId); + d->data->setDeviceId(deviceId); // Can't we deduce this from access_token? + auto* job = callApi<GetTokenOwnerJob>(); + connect(job, &BaseJob::success, this, [this, job, mxId] { + if (mxId != job->userId()) + qCWarning(MAIN).nospace() + << "The access_token owner (" << job->userId() + << ") is different from passed MXID (" << mxId << ")!"; + d->completeSetup(job->userId()); + }); + connect(job, &BaseJob::failure, this, [this, job] { + emit loginError(job->errorString(), job->rawDataSample()); + }); }); } @@ -403,7 +554,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"; }); @@ -425,12 +576,10 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) data->setToken(loginJob->accessToken().toLatin1()); data->setDeviceId(loginJob->deviceId()); completeSetup(loginJob->userId()); -#ifndef Quotient_E2EE_ENABLED - qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; -#else // Quotient_E2EE_ENABLED - encryptionManager->uploadIdentityKeys(q); - encryptionManager->uploadOneTimeKeys(q); -#endif // Quotient_E2EE_ENABLED + saveAccessTokenToKeychain(); +#ifdef Quotient_E2EE_ENABLED + database->clear(); +#endif }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -445,15 +594,63 @@ void Connection::Private::completeSetup(const QString& mxId) qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString() << "by user" << data->userId() << "from device" << data->deviceId(); + Accounts.add(q); + connect(qApp, &QCoreApplication::aboutToQuit, q, &Connection::saveState); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED AccountSettings accountSettings(data->userId()); - encryptionManager.reset( - new EncryptionManager(accountSettings.encryptionAccountPickle())); - if (accountSettings.encryptionAccountPickle().isEmpty()) { - accountSettings.setEncryptionAccountPickle( - encryptionManager->olmAccountPickle()); + + QKeychain::ReadPasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + QEventLoop loop; + QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error() == QKeychain::Error::EntryNotFound) { + picklingMode = Encrypted { RandomBuffer(128) }; + QKeychain::WritePasswordJob job(qAppName()); + job.setAutoDelete(false); + job.setKey(accountSettings.userId() + QStringLiteral("-Pickle")); + job.setBinaryData(std::get<Encrypted>(picklingMode).key); + QEventLoop loop; + QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit); + job.start(); + loop.exec(); + + if (job.error()) { + qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString(); + } + } else if(job.error() != QKeychain::Error::NoError) { + //TODO Error, do something + qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error(); + } else { + qCDebug(E2EE) << "Successfully loaded pickling key from keychain"; + picklingMode = Encrypted { job.binaryData() }; + } + + database = new Database(data->userId(), data->deviceId(), q); + + // init olmAccount + olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q); + connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); + + loadSessions(); + + if (database->accountPickle().isEmpty()) { + // create new account and save unpickle data + olmAccount->createNewAccount(); + auto job = q->callApi<UploadKeysJob>(olmAccount->deviceKeys()); + connect(job, &BaseJob::failure, q, [job]{ + qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString(); + }); + } else { + // account already existing + if (!olmAccount->unpickle(database->accountPickle(), picklingMode)) + qWarning(E2EE) + << "Could not unpickle Olm account, E2EE won't be available"; } #endif // Quotient_E2EE_ENABLED emit q->stateChanged(); @@ -463,7 +660,7 @@ void Connection::Private::completeSetup(const QString& mxId) void Connection::Private::checkAndConnect(const QString& userId, const std::function<void()>& connectFn, - const std::optional<LoginFlows::LoginFlow>& flow) + const std::optional<LoginFlow>& flow) { if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) { connectFn(); @@ -479,10 +676,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); @@ -513,8 +711,10 @@ void Connection::logout() || d->logoutJob->error() == BaseJob::ContentAccessError) { if (d->syncLoopConnection) disconnect(d->syncLoopConnection); - d->data->setToken({}); + SettingsGroup("Accounts").remove(userId()); + d->dropAccessToken(); emit loggedOut(); + deleteLater(); } else { // logout() somehow didn't proceed - restore the session state emit stateChanged(); if (wasSyncing) @@ -606,24 +806,39 @@ QJsonObject toJson(const DirectChatsMap& directChats) void Connection::onSyncSuccess(SyncData&& data, bool fromCache) { +#ifdef Quotient_E2EE_ENABLED + d->oneTimeKeysCount = data.deviceOneTimeKeysCount(); + if (d->oneTimeKeysCount[SignedCurve25519Key] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys() + && !d->isUploadingKeys) { + d->isUploadingKeys = true; + d->olmAccount->generateOneTimeKeys( + d->olmAccount->maxNumberOfOneTimeKeys() / 2 - d->oneTimeKeysCount[SignedCurve25519Key]); + auto keys = d->olmAccount->oneTimeKeys(); + auto job = d->olmAccount->createUploadKeyRequest(keys); + run(job, ForegroundRequest); + connect(job, &BaseJob::success, this, + [this] { d->olmAccount->markKeysAsPublished(); }); + connect(job, &BaseJob::result, this, + [this] { d->isUploadingKeys = false; }); + } + if(d->firstSync) { + d->loadDevicesList(); + d->firstSync = false; + } + + d->consumeDevicesList(data.takeDevicesList()); +#endif // Quotient_E2EE_ENABLED + d->consumeToDeviceEvents(data.takeToDeviceEvents()); d->data->setLastEvent(data.nextBatch()); d->consumeRoomData(data.takeRoomData(), fromCache); d->consumeAccountData(data.takeAccountData()); d->consumePresenceData(data.takePresenceData()); - d->consumeToDeviceEvents(data.takeToDeviceEvents()); #ifdef Quotient_E2EE_ENABLED - // handling device_one_time_keys_count - if (!d->encryptionManager) - { - qCDebug(E2EE) << "Encryption manager is not there yet, updating " - "one-time key counts will be skipped"; - return; + if(d->encryptionUpdateRequired) { + d->loadOutdatedUserDevices(); + d->encryptionUpdateRequired = false; } - if (const auto deviceOneTimeKeysCount = data.deviceOneTimeKeysCount(); - !deviceOneTimeKeysCount.isEmpty()) - d->encryptionManager->updateOneTimeKeyCounts(this, - deviceOneTimeKeysCount); -#endif // Quotient_E2EE_ENABLED +#endif } void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, @@ -641,21 +856,19 @@ 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)) { pendingStateRoomIds.removeOne(roomData.roomId); - r->updateData(std::move(roomData), fromCache); - if (firstTimeRooms.removeOne(r)) { - emit q->loadedRoomState(r); - if (capabilities.roomVersions) - r->checkVersion(); - // Otherwise, the version will be checked in reloadCapabilities() - } + // Update rooms one by one, giving time to update the UI. + QMetaObject::invokeMethod( + r, + [r, rd = std::move(roomData), fromCache] () mutable { + r->updateData(std::move(rd), fromCache); + }, + Qt::QueuedConnection); } - // Let UI update itself after updating each room - QCoreApplication::processEvents(); } } @@ -664,21 +877,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()) @@ -691,7 +904,7 @@ void Connection::Private::consumeAccountData(Events&& accountDataEvents) DirectChatsMap remoteAdditions; for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) { - if (auto* const u = q->user(it.key())) { + if (auto* u = q->user(it.key())) { if (!directChats.contains(u, it.value()) && !dcLocalRemovals.contains(u, it.value())) { Q_ASSERT(!directChatUsers.contains(it.value(), u)); @@ -706,7 +919,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()) @@ -751,34 +964,105 @@ void Connection::Private::consumePresenceData(Events&& presenceData) void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED - // handling m.room_key to-device encrypted event - visitEach(toDeviceEvents, [this](const EncryptedEvent& ee) { - if (ee.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Encrypted event" << ee.id() << "algorithm" - << ee.algorithm() << "is not supported"; - return; + if (!toDeviceEvents.empty()) { + qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() + << "to-device events"; + for (auto&& tdEvt : toDeviceEvents) { + if (processIfVerificationEvent(*tdEvt, false)) + continue; + if (auto&& event = eventCast<EncryptedEvent>(std::move(tdEvt))) { + if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) { + qCDebug(E2EE) << "Unsupported algorithm" << event->id() + << "for event" << event->algorithm(); + return; + } + if (isKnownCurveKey(event->senderId(), event->senderKey())) { + handleEncryptedToDeviceEvent(*event); + return; + } + trackedUsers += event->senderId(); + outdatedUsers += event->senderId(); + encryptionUpdateRequired = true; + pendingEncryptedEvents.push_back(std::move(event)); + } } + } +#endif +} - // TODO: full maintaining of the device keys - // with device_lists sync extention and /keys/query - qCDebug(E2EE) << "Getting device keys for the m.room_key sender:" - << ee.senderId(); - // encryptionManager->updateDeviceKeys(); - - visit(*sessionDecryptMessage(ee), - [this, senderKey = ee.senderKey()](const RoomKeyEvent& roomKeyEvent) { - if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) - detectedRoom->handleRoomKeyEvent(roomKeyEvent, senderKey); - else - qCDebug(E2EE) - << "Encrypted event room id" << roomKeyEvent.roomId() - << "is not found at the connection" << q->objectName(); - }, - [](const Event& evt) { - qCDebug(E2EE) << "Skipping encrypted to_device event, type" - << evt.matrixType(); - }); - }); +#ifdef Quotient_E2EE_ENABLED +bool Connection::Private::processIfVerificationEvent(const Event& evt, + bool encrypted) +{ + return switchOnType(evt, + [this, encrypted](const KeyVerificationRequestEvent& reqEvt) { + const auto sessionIter = verificationSessions.insert( + reqEvt.transactionId(), + new KeyVerificationSession(q->userId(), reqEvt, q, encrypted)); + emit q->newKeyVerificationSession(*sessionIter); + return true; + }, + [](const KeyVerificationDoneEvent&) { + return true; + }, + [this](const KeyVerificationEvent& kvEvt) { + if (auto* const session = + verificationSessions.value(kvEvt.transactionId())) { + session->handleEvent(kvEvt); + emit q->keyVerificationStateChanged(session, session->state()); + } + return true; + }, + false); +} + +void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event) +{ + const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event); + if(!decryptedEvent) { + qCWarning(E2EE) << "Failed to decrypt event" << event.id(); + return; + } + + if (processIfVerificationEvent(*decryptedEvent, true)) + return; + switchOnType(*decryptedEvent, + [this, &event, + olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { + if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { + detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), + olmSessionId); + } else { + qCDebug(E2EE) + << "Encrypted event room id" << roomKeyEvent.roomId() + << "is not found at the connection" << q->objectName(); + } + }, + [](const Event& evt) { + qCWarning(E2EE) << "Skipping encrypted to_device event, type" + << evt.matrixType(); + }); +} +#endif + +void Connection::Private::consumeDevicesList(DevicesList&& devicesList) +{ +#ifdef Quotient_E2EE_ENABLED + bool hasNewOutdatedUser = false; + for(const auto &changed : devicesList.changed) { + if(trackedUsers.contains(changed)) { + outdatedUsers += changed; + hasNewOutdatedUser = true; + } + } + for(const auto &left : devicesList.left) { + trackedUsers -= left; + outdatedUsers -= left; + deviceKeys.remove(left); + } + if(hasNewOutdatedUser) { + loadOutdatedUserDevices(); + } #endif } @@ -796,11 +1080,6 @@ 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) { @@ -844,6 +1123,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) @@ -914,6 +1202,18 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, return job; } +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob* Connection::downloadFile( + const QUrl& url, const EncryptedFileMetadata& fileMetadata, + const QString& localFilename) +{ + auto mediaId = url.authority() + url.path(); + auto idParts = splitMediaId(mediaId); + return callApi<DownloadFileJob>(idParts.front(), idParts.back(), + fileMetadata, localFilename); +} +#endif + CreateRoomJob* Connection::createRoom(RoomVisibility visibility, const QString& alias, const QString& name, const QString& topic, @@ -924,12 +1224,6 @@ Connection::createRoom(RoomVisibility visibility, const QString& alias, const QJsonObject& creationContent) { invites.removeOne(userId()); // The creator is by definition in the room - for (const auto& i : invites) - if (!user(i)) { - qCWarning(MAIN) << "Won't create a room with malformed invitee ids"; - return nullptr; - } - auto job = callApi<CreateRoomJob>(visibility == PublishRoom ? QStringLiteral("public") : QStringLiteral("private"), @@ -964,7 +1258,7 @@ void Connection::requestDirectChat(User* u) void Connection::doInDirectChat(const QString& userId, const std::function<void(Room*)>& operation) { - if (auto* const u = user(userId)) + if (auto* u = user(userId)) doInDirectChat(u, operation); else qCCritical(MAIN) @@ -1060,7 +1354,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 @@ -1079,7 +1373,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 << ": " @@ -1088,26 +1382,11 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) return forgetJob; } -SendToDeviceJob* -Connection::sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap) -{ - QHash<QString, QHash<QString, QJsonObject>> json; - json.reserve(int(eventsMap.size())); - std::for_each(eventsMap.begin(), eventsMap.end(), - [&json](const auto& userTodevicesToEvents) { - auto& jsonUser = json[userTodevicesToEvents.first]; - const auto& devicesToEvents = userTodevicesToEvents.second; - std::for_each(devicesToEvents.begin(), - devicesToEvents.end(), - [&jsonUser](const auto& deviceToEvents) { - jsonUser.insert( - deviceToEvents.first, - deviceToEvents.second.contentJson()); - }); - }); +SendToDeviceJob* Connection::sendToDevices( + const QString& eventType, const UsersToDevicesToContent& contents) +{ return callApi<SendToDeviceJob>(BackgroundRequest, eventType, - generateTxnId(), json); + generateTxnId(), contents); } SendMessageJob* Connection::sendMessage(const QString& roomId, @@ -1200,12 +1479,14 @@ User* Connection::user(const QString& uId) { if (uId.isEmpty()) return nullptr; + if (const auto v = d->userMap.value(uId, nullptr)) + return v; + // Before creating a user object, check that the user id is well-formed + // (it's faster to just do a lookup above before validation) if (!uId.startsWith('@') || serverPart(uId).isEmpty()) { qCCritical(MAIN) << "Malformed userId:" << uId; return nullptr; } - if (d->userMap.contains(uId)) - return d->userMap.value(uId); auto* user = userFactory()(this, uId); d->userMap.insert(uId, user); emit newUser(user); @@ -1227,15 +1508,15 @@ QByteArray Connection::accessToken() const { // The logout job needs access token to do its job; so the token is // kept inside d->data but no more exposed to the outside world. - return isJobRunning(d->logoutJob) ? QByteArray() : d->data->accessToken(); + return isJobPending(d->logoutJob) ? QByteArray() : d->data->accessToken(); } bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); } #ifdef Quotient_E2EE_ENABLED -QtOlm::Account* Connection::olmAccount() const +QOlmAccount *Connection::olmAccount() const { - return d->encryptionManager->account(); + return d->olmAccount.get(); } #endif // Quotient_E2EE_ENABLED @@ -1246,20 +1527,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; @@ -1362,8 +1629,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(); } @@ -1395,7 +1662,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; @@ -1421,8 +1688,8 @@ bool Connection::isIgnored(const User* user) const IgnoredUsersList Connection::ignoredUsers() const { - const auto* event = d->unpackAccountData<IgnoredUsersEvent>(); - return event ? event->ignored_users() : IgnoredUsersList(); + const auto* event = accountData<IgnoredUsersEvent>(); + return event ? event->ignoredUsers() : IgnoredUsersList(); } void Connection::addToIgnoredUsers(const User* user) @@ -1461,7 +1728,7 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState) Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id"); // If joinState is empty, all joinState == comparisons below are false. - const auto roomKey = qMakePair(id, joinState == JoinState::Invite); + const std::pair roomKey { id, joinState == JoinState::Invite }; auto* room = d->roomMap.value(roomKey, nullptr); if (room) { // Leave is a special case because in transition (5a) (see the .h file) @@ -1486,9 +1753,14 @@ Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState) return nullptr; } d->roomMap.insert(roomKey, room); - d->firstTimeRooms.push_back(room); connect(room, &Room::beforeDestruction, this, &Connection::aboutToDeleteRoom); + connect(room, &Room::baseStateLoaded, this, [this, room] { + emit loadedRoomState(room); + if (d->capabilities.roomVersions) + room->checkVersion(); + // Otherwise, the version will be checked in reloadCapabilities() + }); emit newRoom(room); } if (!joinState) @@ -1534,27 +1806,21 @@ 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 { return d->data->generateTxnId(); } -void Connection::Private::clearResolvingContext() -{ - if (isJobRunning(resolverJob)) - resolverJob->abandon(); - if (isJobRunning(loginFlowsJob)) - loginFlowsJob->abandon(); - loginFlows.clear(); - -} - void Connection::setHomeserver(const QUrl& url) { - d->clearResolvingContext(); + if (isJobPending(d->resolverJob)) + d->resolverJob->abandon(); + if (isJobPending(d->loginFlowsJob)) + d->loginFlowsJob->abandon(); + d->loginFlows.clear(); if (homeserver() != url) { d->data->setBaseUrl(url); @@ -1581,16 +1847,10 @@ void Connection::saveRoomState(Room* r) const QFile outRoomFile { stateCacheDir().filePath( SyncData::fileNameForRoom(r->id())) }; if (outRoomFile.open(QFile::WriteOnly)) { -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(r->toJson()).toCbor() : QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { r->toJson() }; - const auto data = d->cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif outRoomFile.write(data.data(), data.size()); qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName(); } else { @@ -1643,26 +1903,26 @@ void Connection::saveState() const } { QJsonArray accountDataEvents { - basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats)) + Event::basicJson(QStringLiteral("m.direct"), toJson(d->directChats)) }; for (const auto& e : d->accountData) accountDataEvents.append( - basicEventJson(e.first, e.second->contentJson())); + Event::basicJson(e.first, e.second->contentJson())); rootObj.insert(QStringLiteral("account_data"), QJsonObject { { QStringLiteral("events"), accountDataEvents } }); } +#ifdef Quotient_E2EE_ENABLED + { + QJsonObject keysJson = toJson(d->oneTimeKeysCount); + rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson); + } +#endif -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) const auto data = d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor() : QJsonDocument(rootObj).toJson(QJsonDocument::Compact); -#else - QJsonDocument json { rootObj }; - const auto data = d->cacheToBinary ? json.toBinaryData() - : json.toJson(QJsonDocument::Compact); -#endif qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et; outFile.write(data.data(), data.size()); @@ -1738,7 +1998,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 = @@ -1763,6 +2023,14 @@ QStringList Connection::stableRoomVersions() const return l; } +bool Connection::canChangePassword() const +{ + // By default assume we can + return d->capabilities.changePassword + ? d->capabilities.changePassword->enabled + : true; +} + inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1, const Connection::SupportedRoomVersion& v2) { @@ -1790,3 +2058,430 @@ QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() co } return result; } + +#ifdef Quotient_E2EE_ENABLED +void Connection::Private::loadOutdatedUserDevices() +{ + QHash<QString, QStringList> users; + for(const auto &user : outdatedUsers) { + users[user] += QStringList(); + } + if(currentQueryKeysJob) { + currentQueryKeysJob->abandon(); + currentQueryKeysJob = nullptr; + } + auto queryKeysJob = q->callApi<QueryKeysJob>(users); + currentQueryKeysJob = queryKeysJob; + connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){ + currentQueryKeysJob = nullptr; + const auto data = queryKeysJob->deviceKeys(); + for(const auto &[user, keys] : asKeyValueRange(data)) { + QHash<QString, Quotient::DeviceKeys> oldDevices = deviceKeys[user]; + deviceKeys[user].clear(); + for(const auto &device : keys) { + if(device.userId != user) { + qCWarning(E2EE) + << "mxId mismatch during device key verification:" + << device.userId << user; + continue; + } + if (!std::all_of(device.algorithms.cbegin(), + device.algorithms.cend(), + isSupportedAlgorithm)) { + qCWarning(E2EE) << "Unsupported encryption algorithms found" + << device.algorithms; + continue; + } + if (!verifyIdentitySignature(device, device.deviceId, + device.userId)) { + qCWarning(E2EE) << "Failed to verify devicekeys signature. " + "Skipping this device"; + continue; + } + if (oldDevices.contains(device.deviceId)) { + if (oldDevices[device.deviceId].keys["ed25519:" % device.deviceId] != device.keys["ed25519:" % device.deviceId]) { + qCDebug(E2EE) << "Device reuse detected. Skipping this device"; + continue; + } + } + deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys); + } + outdatedUsers -= user; + } + saveDevicesList(); + + for(size_t i = 0; i < pendingEncryptedEvents.size();) { + if (isKnownCurveKey( + pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), + pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) { + handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]); + pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i); + } else + ++i; + } + }); +} + +void Connection::Private::saveDevicesList() +{ + q->database()->transaction(); + auto query = q->database()->prepareQuery( + QStringLiteral("DELETE FROM tracked_users")); + q->database()->execute(query); + query.prepare(QStringLiteral( + "INSERT INTO tracked_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : trackedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); + } + + query.prepare(QStringLiteral("DELETE FROM outdated_users")); + q->database()->execute(query); + query.prepare(QStringLiteral( + "INSERT INTO outdated_users(matrixId) VALUES(:matrixId);")); + for (const auto& user : outdatedUsers) { + query.bindValue(":matrixId", user); + q->database()->execute(query); + } + + query.prepare(QStringLiteral( + "INSERT INTO tracked_devices" + "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified) " + "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);" + )); + for (const auto& user : deviceKeys.keys()) { + for (const auto& device : deviceKeys[user]) { + auto keys = device.keys.keys(); + auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1]; + auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1]; + + query.bindValue(":matrixId", user); + query.bindValue(":deviceId", device.deviceId); + query.bindValue(":curveKeyId", curveKeyId); + query.bindValue(":curveKey", device.keys[curveKeyId]); + query.bindValue(":edKeyId", edKeyId); + query.bindValue(":edKey", device.keys[edKeyId]); + // If the device gets saved here, it can't be verified + query.bindValue(":verified", false); + + q->database()->execute(query); + } + } + q->database()->commit(); +} + +void Connection::Private::loadDevicesList() +{ + auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;")); + q->database()->execute(query); + while(query.next()) { + trackedUsers += query.value(0).toString(); + } + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;")); + q->database()->execute(query); + while(query.next()) { + outdatedUsers += query.value(0).toString(); + } + + query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;")); + q->database()->execute(query); + while(query.next()) { + deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys { + query.value("matrixId").toString(), + query.value("deviceId").toString(), + { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}, + {{query.value("curveKeyId").toString(), query.value("curveKey").toString()}, + {query.value("edKeyId").toString(), query.value("edKey").toString()}}, + {} // Signatures are not saved/loaded as they are not needed after initial validation + }; + } + +} + +void Connection::encryptionUpdate(Room *room) +{ + for(const auto &user : room->users()) { + if(!d->trackedUsers.contains(user->id())) { + d->trackedUsers += user->id(); + d->outdatedUsers += user->id(); + d->encryptionUpdateRequired = true; + } + } +} + +PicklingMode Connection::picklingMode() const +{ + return d->picklingMode; +} +#endif + +void Connection::saveOlmAccount() +{ +#ifdef Quotient_E2EE_ENABLED + qCDebug(E2EE) << "Saving olm account"; + d->database->setAccountPickle(d->olmAccount->pickle(d->picklingMode)); +#endif +} + +#ifdef Quotient_E2EE_ENABLED +QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) +{ + if (auto r = room(notification["room_id"].toString())) + if (auto event = + loadEvent<EncryptedEvent>(notification["event"].toObject())) + if (const auto decrypted = r->decryptMessage(*event)) + return decrypted->fullJson(); + return QJsonObject(); +} + +Database* Connection::database() const +{ + return d->database; +} + +UnorderedMap<QString, QOlmInboundGroupSessionPtr> +Connection::loadRoomMegolmSessions(const Room* room) const +{ + return database()->loadMegolmSessions(room->id(), picklingMode()); +} + +void Connection::saveMegolmSession(const Room* room, + const QOlmInboundGroupSession& session) const +{ + database()->saveMegolmSession(room->id(), session.sessionId(), + session.pickle(picklingMode()), + session.senderId(), session.olmSessionId()); +} + +QStringList Connection::devicesForUser(const QString& userId) const +{ + return d->deviceKeys[userId].keys(); +} + +QString Connection::Private::curveKeyForUserDevice(const QString& userId, + const QString& device) const +{ + return deviceKeys[userId][device].keys["curve25519:" % device]; +} + +QString Connection::edKeyForUserDevice(const QString& userId, + const QString& deviceId) const +{ + return d->deviceKeys[userId][deviceId].keys["ed25519:" % deviceId]; +} + +bool Connection::Private::isKnownCurveKey(const QString& userId, + const QString& curveKey) const +{ + auto query = database->prepareQuery( + QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId " + "AND curveKey=:curveKey")); + query.bindValue(":matrixId", userId); + query.bindValue(":curveKey", curveKey); + database->execute(query); + return query.next(); +} + +bool Connection::hasOlmSession(const QString& user, + const QString& deviceId) const +{ + const auto& curveKey = d->curveKeyForUserDevice(user, deviceId); + return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty(); +} + +std::pair<QOlmMessage::Type, QByteArray> Connection::Private::olmEncryptMessage( + const QString& userId, const QString& device, + const QByteArray& message) const +{ + const auto& curveKey = curveKeyForUserDevice(userId, device); + const auto& olmSession = olmSessions.at(curveKey).front(); + const auto result = olmSession->encrypt(message); + database->updateOlmSession(curveKey, olmSession->sessionId(), + olmSession->pickle(picklingMode)); + return { result.type(), result.toCiphertext() }; +} + +bool Connection::Private::createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const OneTimeKeys& oneTimeKeyObject) +{ + static QOlmUtility verifier; + qDebug(E2EE) << "Creating a new session for" << targetUserId + << targetDeviceId; + if (oneTimeKeyObject.isEmpty()) { + qWarning(E2EE) << "No one time key for" << targetUserId + << targetDeviceId; + return false; + } + auto* signedOneTimeKey = + std::get_if<SignedOneTimeKey>(&*oneTimeKeyObject.begin()); + if (!signedOneTimeKey) { + qWarning(E2EE) << "No signed one time key for" << targetUserId + << targetDeviceId; + return false; + } + // Verify contents of signedOneTimeKey - for that, drop `signatures` and + // `unsigned` and then verify the object against the respective signature + const auto signature = + signedOneTimeKey->signature(targetUserId, targetDeviceId); + if (!verifier.ed25519Verify( + q->edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(), + signedOneTimeKey->toJsonForVerification(), + signature)) { + qWarning(E2EE) << "Failed to verify one-time-key signature for" + << targetUserId << targetDeviceId + << ". Skipping this device."; + return false; + } + const auto recipientCurveKey = + curveKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(); + auto session = + QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey, + signedOneTimeKey->key()); + if (!session) { + qCWarning(E2EE) << "Failed to create olm session for " + << recipientCurveKey << session.error(); + return false; + } + saveSession(**session, recipientCurveKey); + olmSessions[recipientCurveKey].push_back(std::move(*session)); + return true; +} + +QJsonObject Connection::Private::assembleEncryptedContent( + QJsonObject payloadJson, const QString& targetUserId, + const QString& targetDeviceId) const +{ + payloadJson.insert(SenderKeyL, data->userId()); +// eventJson.insert("sender_device"_ls, data->deviceId()); + payloadJson.insert("keys"_ls, + QJsonObject{ + { Ed25519Key, + QString(olmAccount->identityKeys().ed25519) } }); + payloadJson.insert("recipient"_ls, targetUserId); + payloadJson.insert( + "recipient_keys"_ls, + QJsonObject{ { Ed25519Key, + q->edKeyForUserDevice(targetUserId, targetDeviceId) } }); + const auto [type, cipherText] = olmEncryptMessage( + targetUserId, targetDeviceId, + QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + QJsonObject encrypted { + { curveKeyForUserDevice(targetUserId, targetDeviceId), + QJsonObject { { "type"_ls, type }, + { "body"_ls, QString(cipherText) } } } + }; + return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519) + .contentJson(); +} + +void Connection::sendSessionKeyToDevices( + const QString& roomId, const QByteArray& sessionId, + const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices, + int index) +{ + qDebug(E2EE) << "Sending room key to devices:" << sessionId + << sessionKey.toHex(); + QHash<QString, QHash<QString, QString>> hash; + for (const auto& [userId, deviceId] : asKeyValueRange(devices)) + if (!hasOlmSession(userId, deviceId)) { + hash[userId].insert(deviceId, "signed_curve25519"_ls); + qDebug(E2EE) << "Adding" << userId << deviceId + << "to keys to claim"; + } + + if (hash.isEmpty()) + return; + + auto job = callApi<ClaimKeysJob>(hash); + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] { + QHash<QString, QHash<QString, QJsonObject>> usersToDevicesToContent; + for (const auto oneTimeKeys = job->oneTimeKeys(); + const auto& [targetUserId, targetDeviceId] : + asKeyValueRange(devices)) { + if (!hasOlmSession(targetUserId, targetDeviceId) + && !d->createOlmSession( + targetUserId, targetDeviceId, + oneTimeKeys[targetUserId][targetDeviceId])) + continue; + + // Noisy but nice for debugging +// qDebug(E2EE) << "Creating the payload for" << targetUserId +// << targetDeviceId << sessionId << sessionKey.toHex(); + const auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, + roomId, sessionId, sessionKey) + .fullJson(); + + usersToDevicesToContent[targetUserId][targetDeviceId] = + d->assembleEncryptedContent(keyEventJson, targetUserId, + targetDeviceId); + } + if (!usersToDevicesToContent.empty()) { + sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent); + QVector<std::tuple<QString, QString, QString>> receivedDevices; + receivedDevices.reserve(devices.size()); + for (const auto& [user, device] : asKeyValueRange(devices)) + receivedDevices.push_back( + { user, device, d->curveKeyForUserDevice(user, device) }); + + database()->setDevicesReceivedKey(roomId, receivedDevices, + sessionId, index); + } + }); +} + +QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession( + const QString& roomId) const +{ + return d->database->loadCurrentOutboundMegolmSession(roomId, + d->picklingMode); +} + +void Connection::saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const +{ + d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode, + session); +} + +void Connection::startKeyVerificationSession(const QString& deviceId) +{ + auto* const session = new KeyVerificationSession(userId(), deviceId, this); + emit newKeyVerificationSession(session); +} + +void Connection::sendToDevice(const QString& targetUserId, + const QString& targetDeviceId, const Event& event, + bool encrypted) +{ + const auto contentJson = + encrypted ? d->assembleEncryptedContent(event.fullJson(), targetUserId, + targetDeviceId) + : event.contentJson(); + sendToDevices(encrypted ? EncryptedEvent::TypeId : event.type(), + { { targetUserId, { { targetDeviceId, contentJson } } } }); +} + +bool Connection::isVerifiedSession(const QString& megolmSessionId) const +{ + auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", megolmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto olmSessionId = query.value("olmSessionId").toString(); + query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls); + query.bindValue(":sessionId", olmSessionId); + database()->execute(query); + if (!query.next()) { + return false; + } + auto curveKey = query.value("senderKey"_ls).toString(); + query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls); + query.bindValue(":curveKey", curveKey); + database()->execute(query); + return query.next() && query.value("verified").toBool(); +} +#endif diff --git a/lib/connection.h b/lib/connection.h index c90cb892..75faf370 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -1,30 +1,16 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "ssosession.h" -#include "joinstate.h" -#include "qt_connection_util.h" #include "quotient_common.h" +#include "ssosession.h" +#include "util.h" -#include "csapi/login.h" #include "csapi/create_room.h" +#include "csapi/login.h" #include "events/accountdataevents.h" @@ -35,9 +21,12 @@ #include <functional> -namespace QtOlm { -class Account; -} +#ifdef Quotient_E2EE_ENABLED +#include "e2ee/e2ee.h" +#include "e2ee/qolmoutboundsession.h" +#include "keyverificationsession.h" +#include "events/keyverificationevent.h" +#endif Q_DECLARE_METATYPE(Quotient::GetLoginFlowsJob::LoginFlow) @@ -61,29 +50,33 @@ class DownloadFileJob; class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; +class Database; +struct EncryptedFileMetadata; + +class QOlmAccount; +class QOlmInboundGroupSession; + +using LoginFlow = GetLoginFlowsJob::LoginFlow; + +/// Predefined login flows +namespace LoginFlows { + inline const LoginFlow Password { "m.login.password" }; + inline const LoginFlow SSO { "m.login.sso" }; + inline const LoginFlow Token { "m.login.token" }; +} // To simplify comparisons of LoginFlows -inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) +inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs) { return lhs.type == rhs.type; } -inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) +inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs) { return !(lhs == rhs); } -/// Predefined login flows -struct LoginFlows { - using LoginFlow = GetLoginFlowsJob::LoginFlow; - static inline const LoginFlow Password { "m.login.password" }; - static inline const LoginFlow SSO { "m.login.sso" }; - static inline const LoginFlow Token { "m.login.token" }; -}; - class Connection; using room_factory_t = @@ -96,11 +89,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 @@ -109,9 +100,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 @@ -120,9 +111,9 @@ static inline user_factory_t defaultUserFactory() // are stored with no regard to their state. using DirectChatsMap = QMultiHash<const User*, QString>; using DirectChatUsersMap = QMultiHash<QString, User*>; -using IgnoredUsersList = IgnoredUsersEvent::content_type; +using IgnoredUsersList = IgnoredUsersEvent::value_type; -class Connection : public QObject { +class QUOTIENT_API Connection : public QObject { Q_OBJECT Q_PROPERTY(User* localUser READ user NOTIFY stateChanged) @@ -139,10 +130,10 @@ 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 = - UnorderedMap<QString, UnorderedMap<QString, const Event&>>; + using UsersToDevicesToContent = QHash<QString, QHash<QString, QJsonObject>>; enum RoomVisibility { PublishRoom, @@ -153,15 +144,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. @@ -192,24 +174,25 @@ public: */ bool hasAccountData(const QString& type) const; - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ + //! \brief Get a generic account data event of the given type + //! + //! \return an account data event of the given type stored on the server, + //! or nullptr if there's none of that type. + //! \note Direct chats map cannot be retrieved using this method _yet_; + //! use directChats() instead. const EventPtr& accountData(const QString& type) const; - /** Get a generic account data event of the given type - * This returns an account data event of the given type - * stored on the server. Direct chats map cannot be retrieved - * using this method _yet_; use directChats() instead. - */ - template <typename EventT> - const typename EventT::content_type accountData() const + //! \brief Get an account data event of the given type + //! + //! \return the account data content for the given event type stored + //! on the server, or a default-constructed object if there's none + //! of that type. + //! \note Direct chats map cannot be retrieved using this method _yet_; + //! use directChats() instead. + template <EventClass EventT> + const EventT* accountData() const { - if (const auto& eventPtr = accountData(EventT::matrixTypeId())) - return eventPtr->content(); - return {}; + return eventCast<EventT>(accountData(EventT::TypeId)); } /** Get account data as a JSON object @@ -334,7 +317,38 @@ public: QByteArray accessToken() const; bool isLoggedIn() const; #ifdef Quotient_E2EE_ENABLED - QtOlm::Account* olmAccount() const; + QOlmAccount* olmAccount() const; + Database* database() const; + PicklingMode picklingMode() const; + + UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions( + const Room* room) const; + void saveMegolmSession(const Room* room, + const QOlmInboundGroupSession& session) const; + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( + const QString& roomId) const; + void saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const; + + QString edKeyForUserDevice(const QString& userId, + const QString& deviceId) const; + bool hasOlmSession(const QString& user, const QString& deviceId) const; + + // This assumes that an olm session already exists. If it doesn't, no message is sent. + void sendToDevice(const QString& targetUserId, const QString& targetDeviceId, + const Event& event, bool encrypted); + + /// Returns true if this megolm session comes from a verified device + bool isVerifiedSession(const QString& megolmSessionId) const; + + void sendSessionKeyToDevices(const QString& roomId, + const QByteArray& sessionId, + const QByteArray& sessionKey, + const QMultiHash<QString, QString>& devices, + int index); + + QJsonObject decryptNotification(const QJsonObject ¬ification); + QStringList devicesForUser(const QString& userId) const; #endif // Quotient_E2EE_ENABLED Q_INVOKABLE Quotient::SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; @@ -368,22 +382,19 @@ public: * \sa loadingCapabilities */ QVector<SupportedRoomVersion> availableRoomVersions() const; + /// Indicate if the user can change its password from the client. + /// This is often not the case when SSO is enabled. + /// \sa loadingCapabilities + bool canChangePassword() const; + /** * Call this before first sync to load from previously saved file. - * - * \param fromFile A local path to read the state from. Uses QUrl - * to be QML-friendly. Empty parameter means saving to the directory - * defined by stateCachePath() / stateCacheDir(). */ Q_INVOKABLE void loadState(); /** * This method saves the current state of rooms (but not messages * in them) to a local cache file, so that it could be loaded by * loadState() on a next run of the client. - * - * \param toFile A local path to save the state to. Uses QUrl to be - * QML-friendly. Empty parameter means saving to the directory - * defined by stateCachePath() / stateCacheDir(). */ Q_INVOKABLE void saveState() const; @@ -418,7 +429,7 @@ public: /*! Start a pre-created job object on this connection */ Q_INVOKABLE BaseJob* run(BaseJob* job, - RunningPolicy runningPolicy = ForegroundRequest); + RunningPolicy runningPolicy = ForegroundRequest); /*! Start a job of a specified type with specified arguments and policy * @@ -463,6 +474,17 @@ public: std::forward<JobArgTs>(jobArgs)...); } + //! \brief Start a local HTTP server and generate a single sign-on URL + //! + //! This call does the preparatory steps to carry out single sign-on + //! sequence + //! \sa https://matrix.org/docs/guides/sso-for-client-developers + //! \return A proxy object holding two URLs: one for SSO on the chosen + //! homeserver and another for the local callback address. Normally + //! you won't need the callback URL unless you proxy the response + //! with a custom UI. You do not need to delete the SsoSession + //! object; the Connection that issued it will dispose of it once + //! the login sequence completes (with any outcome). Q_INVOKABLE SsoSession* prepareForSso(const QString& initialDeviceName, const QString& deviceId = {}); @@ -487,21 +509,37 @@ 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 slots: - /** Set the homeserver base URL */ + /// Saves the olm account data to disk. Usually doesn't need to be called manually. + void saveOlmAccount(); + +public Q_SLOTS: + /// \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 @@ -534,30 +572,12 @@ public 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); @@ -566,6 +586,8 @@ public slots: void stopSync(); QString nextBatchToken() const; + Q_INVOKABLE QUrl makeMediaUrl(QUrl mxcUrl) const; + virtual MediaThumbnailJob* getThumbnail(const QString& mediaId, QSize requestedSize, RunningPolicy policy = BackgroundRequest); @@ -587,6 +609,11 @@ public slots: DownloadFileJob* downloadFile(const QUrl& url, const QString& localFilename = {}); +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob* downloadFile(const QUrl& url, + const EncryptedFileMetadata& fileMetadata, + const QString& localFilename = {}); +#endif /** * \brief Create a room (generic method) * This method allows to customize room entirely to your liking, @@ -664,7 +691,7 @@ public slots: ForgetRoomJob* forgetRoom(const QString& id); SendToDeviceJob* sendToDevices(const QString& eventType, - const UsersToDevicesToEvents& eventsMap); + const UsersToDevicesToContent& contents); /** \deprecated This method is experimental and may be removed any time */ SendMessageJob* sendMessage(const QString& roomId, const RoomEvent& event); @@ -672,24 +699,18 @@ public 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. +#ifdef Quotient_E2EE_ENABLED + void startKeyVerificationSession(const QString& deviceId); - /** @deprecated Use callApi<PostReceiptJob>() or Room::postReceipt() instead - */ - virtual PostReceiptJob* postReceipt(Room* room, RoomEvent* event); + void encryptionUpdate(Room *room); +#endif -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(); +Q_SIGNALS: + /// \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); @@ -697,7 +718,6 @@ signals: void capabilitiesLoaded(); void connected(); - void reconnected(); //< \deprecated Use connected() instead void loggedOut(); /** Login data or state have changed * @@ -841,6 +861,15 @@ signals: void cacheStateChanged(); void lazyLoadingChanged(); void turnServersChanged(const QJsonObject& servers); + void devicesListLoaded(); + +#ifdef Quotient_E2EE_ENABLED + void newKeyVerificationSession(KeyVerificationSession* session); + void keyVerificationStateChanged( + const KeyVerificationSession* session, + Quotient::KeyVerificationSession::State state); + void sessionVerified(const QString& userId, const QString& deviceId); +#endif protected: /** @@ -875,12 +904,12 @@ protected: */ void onSyncSuccess(SyncData&& data, bool fromCache = false); -protected slots: +protected Q_SLOTS: void syncLoopIteration(); private: class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; static room_factory_t _roomFactory; static user_factory_t _userFactory; diff --git a/lib/connectiondata.cpp b/lib/connectiondata.cpp index d57363d0..aca218be 100644 --- a/lib/connectiondata.cpp +++ b/lib/connectiondata.cpp @@ -1,20 +1,6 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "connectiondata.h" @@ -55,7 +41,7 @@ public: }; ConnectionData::ConnectionData(QUrl baseUrl) - : d(std::make_unique<Private>(std::move(baseUrl))) + : d(makeImpl<Private>(std::move(baseUrl))) { // Each lambda invocation below takes no more than one job from the // queues (first foreground, then background) and resumes it; then @@ -132,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 000099d1..75fc332f 100644 --- a/lib/connectiondata.h +++ b/lib/connectiondata.h @@ -1,26 +1,13 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "util.h" + #include <QtCore/QUrl> -#include <memory> #include <chrono> class QNetworkAccessManager; @@ -45,10 +32,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); @@ -60,6 +43,6 @@ public: private: class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient diff --git a/lib/converters.cpp b/lib/converters.cpp index 9f570087..b0e3a4b6 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -1,43 +1,41 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "converters.h" +#include "logging.h" #include <QtCore/QVariant> -using namespace Quotient; +void Quotient::_impl::warnUnknownEnumValue(const QString& stringValue, + const char* enumTypeName) +{ + qWarning(EVENTS).noquote() + << "Unknown" << enumTypeName << "value:" << stringValue; +} + +void Quotient::_impl::reportEnumOutOfBounds(uint32_t v, const char* enumTypeName) +{ + qCritical(MAIN).noquote() + << "Value" << v << "is out of bounds for enumeration" << enumTypeName; +} -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 543e9496..0fb36320 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -1,23 +1,9 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "omittable.h" #include "util.h" #include <QtCore/QDate> @@ -28,163 +14,312 @@ #include <QtCore/QUrlQuery> #include <QtCore/QVector> +#include <type_traits> #include <vector> +#include <variant> class QVariant; namespace Quotient { template <typename T> struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); } - static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } + // To be implemented in specialisations + static void dumpTo(QJsonObject&, const T&) = delete; + static void fillFrom(const QJsonObject&, T&) = delete; +}; + +template <typename PodT, typename JsonT> +PodT fromJson(const JsonT&); + +template <typename T> +struct JsonObjectUnpacker { + // By default, revert to fromJson() so that one could provide a single + // fromJson<T, QJsonObject> specialisation instead of specialising + // the entire JsonConverter; if a different type of JSON value is needed + // (e.g., an array), specialising JsonConverter is inevitable + static T load(const QJsonValue& jv) { return fromJson<T>(jv.toObject()); } + static T load(const QJsonDocument& jd) { return fromJson<T>(jd.object()); } }; +//! \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) +struct JsonConverter : JsonObjectUnpacker<T> { + // Unfortunately, if constexpr doesn't work with dump() and T::toJson + // because trying to check invocability of T::toJson hits a hard + // (non-SFINAE) compilation error if the member is not there. Hence a bit + // more verbose SFINAE construct in _impl::JsonExporter. + static auto dump(const T& data) { - QJsonObject jo; - JsonObjectConverter<T>::dumpTo(jo, pod); - return jo; + if constexpr (requires() { data.toJson(); }) + return data.toJson(); + else { + QJsonObject jo; + JsonObjectConverter<T>::dumpTo(jo, data); + return jo; + } } - static T doLoad(const QJsonObject& jo) + + using JsonObjectUnpacker<T>::load; + static T load(const QJsonObject& jo) { - T pod; - JsonObjectConverter<T>::fillFrom(jo, pod); - return pod; + // 'else' below are required to suppress code generation for unused + // branches - 'return' is not enough + if constexpr (std::is_same_v<T, QJsonObject>) + return jo; + else if constexpr (std::is_constructible_v<T, QJsonObject>) + return T(jo); + else { + T pod; + JsonObjectConverter<T>::fillFrom(jo, pod); + return pod; + } } - static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } - static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } }; template <typename T> inline auto toJson(const T& pod) +// -> can return anything from which QJsonValue or, in some cases, QJsonDocument +// is constructible { - return JsonConverter<T>::dump(pod); + if constexpr (std::is_constructible_v<QJsonValue, T>) + return pod; // No-op if QJsonValue can be directly constructed + else + return JsonConverter<T>::dump(pod); } -inline auto toJson(const QJsonObject& jo) { return jo; } - template <typename T> inline void fillJson(QJsonObject& json, const T& data) { JsonObjectConverter<T>::dumpTo(json, data); } -template <typename T> -inline T fromJson(const QJsonValue& jv) +template <typename PodT, typename JsonT> +inline PodT fromJson(const JsonT& json) +{ + // JsonT here can be whatever the respective JsonConverter specialisation + // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject + return JsonConverter<PodT>::load(json); +} + +// Convenience fromJson() overload that deduces PodT instead of requiring +// the coder to explicitly type it. It still enforces the +// overwrite-everything semantics of fromJson(), unlike fillFromJson() + +template <typename JsonT, typename PodT> +inline void fromJson(const JsonT& json, PodT& pod) { - return JsonConverter<T>::load(jv); + pod = fromJson<PodT>(json); } template <typename T> -inline T fromJson(const QJsonDocument& jd) +inline void fillFromJson(const QJsonValue& jv, T& pod) { - return JsonConverter<T>::load(jd); + if constexpr (requires() { JsonObjectConverter<T>::fillFrom({}, pod); }) { + JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); + return; + } else if (!jv.isUndefined()) + pod = fromJson<T>(jv); } -// Convenience fromJson() overloads that deduce T instead of requiring -// the coder to explicitly type it. They still enforce the -// overwrite-everything semantics of fromJson(), unlike fillFromJson() +namespace _impl { + void warnUnknownEnumValue(const QString& stringValue, + const char* enumTypeName); + void reportEnumOutOfBounds(uint32_t v, const char* enumTypeName); +} -template <typename T> -inline void fromJson(const QJsonValue& jv, T& pod) +//! \brief Facility string-to-enum converter +//! +//! This is to simplify enum loading from JSON - just specialise +//! Quotient::fromJson() and call this function from it, passing (aside from +//! the JSON value for the enum - that must be a string, not an int) any +//! iterable container of string'y values (const char*, QLatin1String, etc.) +//! matching respective enum values, 0-based. +//! \sa enumToJsonString +template <typename EnumT, typename EnumStringValuesT> +EnumT enumFromJsonString(const QString& s, const EnumStringValuesT& enumValues, + EnumT defaultValue) { - pod = jv.isUndefined() ? T() : fromJson<T>(jv); + static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>); + if (const auto it = std::find(cbegin(enumValues), cend(enumValues), s); + it != cend(enumValues)) + return EnumT(it - cbegin(enumValues)); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(EnumT())); + return defaultValue; } -template <typename T> -inline void fromJson(const QJsonDocument& jd, T& pod) +//! \brief Facility enum-to-string converter +//! +//! This does the same as enumFromJsonString, the other way around. +//! \note The source enumeration must not have gaps in values, or \p enumValues +//! has to match those gaps (i.e., if the source enumeration is defined +//! as <tt>{ Value1 = 1, Value2 = 3, Value3 = 5 }</tt> then \p enumValues +//! should be defined as <tt>{ "", "Value1", "", "Value2", "", "Value3" +//! }</tt> (mind the gap at value 0, in particular). +//! \sa enumFromJsonString +template <typename EnumT, typename EnumStringValuesT> +QString enumToJsonString(EnumT v, const EnumStringValuesT& enumValues) { - pod = fromJson<T>(jd); + static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>); + if (v < size(enumValues)) + return enumValues[v]; + + _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), + qt_getEnumName(EnumT())); + Q_ASSERT(false); + return {}; } -template <typename T> -inline void fillFromJson(const QJsonValue& jv, T& pod) +//! \brief Facility converter for flags +//! +//! This is very similar to enumFromJsonString, except that the target +//! enumeration is assumed to be of a 'flag' kind - i.e. its values must be +//! a power-of-two sequence starting from 1, without gaps, so exactly 1,2,4,8,16 +//! and so on. +//! \note Unlike enumFromJsonString, the values start from 1 and not from 0, +//! with 0 being used for an invalid value by default. +//! \note This function does not support flag combinations. +//! \sa QUO_DECLARE_FLAGS, QUO_DECLARE_FLAGS_NS +template <typename FlagT, typename FlagStringValuesT> +FlagT flagFromJsonString(const QString& s, const FlagStringValuesT& flagValues, + FlagT defaultValue = FlagT(0U)) { - if (jv.isObject()) - JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); - else if (!jv.isUndefined()) - pod = fromJson<T>(jv); + // Enums based on signed integers don't make much sense for flag types + static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>); + if (const auto it = std::find(cbegin(flagValues), cend(flagValues), s); + it != cend(flagValues)) + return FlagT(1U << (it - cbegin(flagValues))); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(FlagT())); + return defaultValue; } -// JsonConverter<> specialisations +template <typename FlagT, typename FlagStringValuesT> +QString flagToJsonString(FlagT v, const FlagStringValuesT& flagValues) +{ + static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>); + if (const auto offset = + qCountTrailingZeroBits(std::underlying_type_t<FlagT>(v)); + offset < size(flagValues)) // + { + return flagValues[offset]; + } -template <typename T> -struct TrivialJsonDumper { - // Works for: QJsonValue (and all things it can consume), - // QJsonObject, QJsonArray - static auto dump(const T& val) { return val; } -}; + _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), + qt_getEnumName(FlagT())); + Q_ASSERT(false); + return {}; +} + +// Specialisations + +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(); } +//! Use fromJson<QString> and then toLatin1()/toUtf8()/... to make QByteArray +//! +//! QJsonValue can only convert to QString and there's ambiguity whether +//! conversion to QByteArray should use (fast but very limited) toLatin1() or +//! (all encompassing and conforming to the JSON spec but slow) toUtf8(). template <> -struct JsonConverter<QString> : public TrivialJsonDumper<QString> { - static auto load(const QJsonValue& jv) { return jv.toString(); } -}; +inline QByteArray fromJson(const QJsonValue& jv) = delete; 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 QJsonValue& jv) { return jv.toArray(); } 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 QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); } +inline QJsonValue toJson(const QDateTime& val) +{ + return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); +} template <> -struct JsonConverter<QJsonArray> : public TrivialJsonDumper<QJsonArray> { - static auto load(const QJsonValue& jv) { return jv.toArray(); } -}; +inline QDateTime fromJson(const QJsonValue& jv) +{ + return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); +} +inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); } template <> -struct JsonConverter<QByteArray> { - static QString dump(const QByteArray& ba) { return ba.constData(); } +inline QDate fromJson(const QJsonValue& jv) +{ + return fromJson<QDateTime>(jv).date(); +} + +// 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<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); } }; template <> -struct JsonConverter<QVariant> { +struct QUOTIENT_API JsonConverter<QVariant> { static QJsonValue dump(const QVariant& v); static QVariant load(const QJsonValue& jv); }; +template <typename... Ts> +inline QJsonValue toJson(const std::variant<Ts...>& v) +{ + // std::visit requires all overloads to return the same type - and + // QJsonValue is a perfect candidate for that same type (assuming that + // variants never occur on the top level in Matrix API) + return std::visit( + [](const auto& value) { return QJsonValue { toJson(value) }; }, v); +} + +template <typename T> +struct JsonConverter<std::variant<QString, T>> { + static std::variant<QString, T> load(const QJsonValue& jv) + { + if (jv.isString()) + return fromJson<QString>(jv); + return fromJson<T>(jv); + } +}; + template <typename T> struct JsonConverter<Omittable<T>> { static QJsonValue dump(const Omittable<T>& from) @@ -201,23 +336,23 @@ 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) { VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); - for (const auto& i : ja) - vect.push_back(fromJson<T>(i)); + // NB: Make sure to pass QJsonValue to fromJson<> so that it could + // hit the correct overload and not fall back to the generic fromJson + // that treats everything as an object. See also the explanation in + // the commit introducing these lines. + for (const QJsonValue v : ja) + vect.push_back(fromJson<T>(v)); return vect; } static auto load(const QJsonValue& jv) { return load(jv.toArray()); } @@ -228,14 +363,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); @@ -247,14 +384,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; } }; @@ -267,9 +403,12 @@ struct HashMapFromJson { } static void fillFrom(const QJsonObject& jo, HashMapT& h) { - h.reserve(jo.size()); + h.reserve(h.size() + jo.size()); + // NB: the QJsonValue cast below is for the same reason as in + // JsonArrayConverter for (auto it = jo.begin(); it != jo.end(); ++it) - h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value()); + h[it.key()] = fromJson<typename HashMapT::mapped_type>( + QJsonValue(it.value())); } }; @@ -281,14 +420,14 @@ template <typename T> struct JsonObjectConverter<QHash<QString, T>> : public HashMapFromJson<QHash<QString, T>> {}; +QJsonObject QUOTIENT_API toJson(const QVariantHash& vh); template <> -struct JsonConverter<QVariantHash> { - static QJsonObject dump(const QVariantHash& vh); - static QVariantHash load(const QJsonValue& jv); -}; +QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject +constexpr bool IfNotEmpty = false; + namespace _impl { template <typename ValT> inline void addTo(QJsonObject& o, const QString& k, ValT&& v) @@ -309,16 +448,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 @@ -335,7 +473,7 @@ namespace _impl { // This one is for types that have isEmpty() when Force is false template <typename ValT> - struct AddNode<ValT, false, decltype(std::declval<ValT>().isEmpty())> { + struct AddNode<ValT, IfNotEmpty, decltype(std::declval<ValT>().isEmpty())> { template <typename ContT, typename ForwardedT> static void impl(ContT& container, const QString& key, ForwardedT&& value) @@ -345,9 +483,9 @@ namespace _impl { } }; - // This one unfolds Omittable<> (also only when Force is false) + // This one unfolds Omittable<> (also only when IfNotEmpty is requested) template <typename ValT> - struct AddNode<Omittable<ValT>, false> { + struct AddNode<Omittable<ValT>, IfNotEmpty> { template <typename ContT, typename OmittableT> static void impl(ContT& container, const QString& key, const OmittableT& value) @@ -358,8 +496,6 @@ namespace _impl { }; } // namespace _impl -static constexpr bool IfNotEmpty = false; - /*! Add a key-value pair to QJsonObject or QUrlQuery * * Adds a key-value pair(s) specified by \p key and \p value to @@ -389,4 +525,20 @@ inline void addParam(ContT& container, const QString& key, ValT&& value) _impl::AddNode<std::decay_t<ValT>, Force>::impl(container, key, std::forward<ValT>(value)); } + +// This is a facility function to convert camelCase method/variable names +// used throughout Quotient to snake_case JSON keys - see usage in +// single_key_value.h and event.h (DEFINE_CONTENT_GETTER macro). +inline auto toSnakeCase(QLatin1String s) +{ + QString result { s }; + for (auto it = result.begin(); it != result.end(); ++it) + if (it->isUpper()) { + const auto offset = static_cast<int>(it - result.begin()); + result.insert(offset, '_'); // NB: invalidates iterators + it = result.begin() + offset + 1; + *it = it->toLower(); + } + return result; +} } // namespace Quotient diff --git a/lib/csapi/account-data.cpp b/lib/csapi/account-data.cpp index 6a40e908..8c71f6c5 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/v3", "/user/", userId, "/account_data/", + type)) { - setRequestData(Data(toJson(content))); + setRequestData({ 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/v3", "/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/v3", "/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/v3", "/user/", userId, "/rooms/", + roomId, "/account_data/", type)) { - setRequestData(Data(toJson(content))); + setRequestData({ 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/v3", "/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/v3", "/user/", userId, "/rooms/", + roomId, "/account_data/", type)) {} diff --git a/lib/csapi/account-data.h b/lib/csapi/account-data.h index 0c760e80..70d4e492 100644 --- a/lib/csapi/account-data.h +++ b/lib/csapi/account-data.h @@ -8,46 +8,47 @@ namespace Quotient { -/*! \brief Set some account_data for the user. +/*! \brief Set some account data for the user. * - * Set some account_data for the client. This config is only visible to the user - * that set the account_data. The config will be synced to clients in the - * top-level `account_data`. + * Set some account data for the client. This config is only visible to the user + * that set the account data. The config will be available to clients through + * the top-level `account_data` field in the homeserver response to + * [/sync](#get_matrixclientv3sync). */ -class SetAccountDataJob : public BaseJob { +class QUOTIENT_API SetAccountDataJob : public BaseJob { public: - /*! \brief Set some account_data for the user. + /*! \brief Set some account data for the user. * * \param userId - * The ID of the user to set account_data for. The access token must be + * The ID of the user to set account data for. The access token must be * authorized to make requests for this user ID. * * \param type - * The event type of the account_data to set. Custom types should be + * The event type of the account data to set. Custom types should be * namespaced to avoid clashes. * * \param content - * The content of the account_data + * The content of the account data. */ explicit SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content = {}); }; -/*! \brief Get some account_data for the user. +/*! \brief Get some account data for the user. * - * Get some account_data for the client. This config is only visible to the user - * that set the account_data. + * Get some account data for the client. This config is only visible to the user + * that set the account data. */ -class GetAccountDataJob : public BaseJob { +class QUOTIENT_API GetAccountDataJob : public BaseJob { public: - /*! \brief Get some account_data for the user. + /*! \brief Get some account data for the user. * * \param userId - * The ID of the user to get account_data for. The access token must be + * The ID of the user to get account data for. The access token must be * authorized to make requests for this user ID. * * \param type - * The event type of the account_data to get. Custom types should be + * The event type of the account data to get. Custom types should be * namespaced to avoid clashes. */ explicit GetAccountDataJob(const QString& userId, const QString& type); @@ -61,53 +62,53 @@ public: const QString& type); }; -/*! \brief Set some account_data for the user. +/*! \brief Set some account data for the user that is specific to a room. * - * 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`. + * 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 delivered + * to clients in the per-room entries via [/sync](#get_matrixclientv3sync). */ -class SetAccountDataPerRoomJob : public BaseJob { +class QUOTIENT_API SetAccountDataPerRoomJob : public BaseJob { public: - /*! \brief Set some account_data for the user. + /*! \brief Set some account data for the user that is specific to a room. * * \param userId - * The ID of the user to set account_data for. The access token must be + * The ID of the user to set account data for. The access token must be * authorized to make requests for this user ID. * * \param roomId - * The ID of the room to set account_data on. + * The ID of the room to set account data on. * * \param type - * The event type of the account_data to set. Custom types should be + * The event type of the account data to set. Custom types should be * namespaced to avoid clashes. * * \param content - * The content of the account_data + * The content of the account data. */ explicit SetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type, const QJsonObject& content = {}); }; -/*! \brief Get some account_data for the user. +/*! \brief Get some account data for the user that is specific to a room. * - * Get some account_data for the client on a given room. This config is only - * visible to the user that set the account_data. + * Get some account data for the client on a given room. This config is only + * visible to the user that set the account data. */ -class GetAccountDataPerRoomJob : public BaseJob { +class QUOTIENT_API GetAccountDataPerRoomJob : public BaseJob { public: - /*! \brief Get some account_data for the user. + /*! \brief Get some account data for the user that is specific to a room. * * \param userId - * The ID of the user to set account_data for. The access token must be + * The ID of the user to get account data for. The access token must be * authorized to make requests for this user ID. * * \param roomId - * The ID of the room to get account_data for. + * The ID of the room to get account data for. * * \param type - * The event type of the account_data to get. Custom types should be + * The event type of the account data to get. Custom types should be * namespaced to avoid clashes. */ explicit GetAccountDataPerRoomJob(const QString& userId, diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 9619c441..322212db 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/v3", + "/admin/whois/", userId)); } GetWhoIsJob::GetWhoIsJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"), - QStringLiteral("/_matrix/client/r0") % "/admin/whois/" % userId) + makePath("/_matrix/client/v3", "/admin/whois/", userId)) {} diff --git a/lib/csapi/admin.h b/lib/csapi/admin.h index 570bf24a..c53ddd7e 100644 --- a/lib/csapi/admin.h +++ b/lib/csapi/admin.h @@ -16,7 +16,7 @@ namespace Quotient { * up, or by a server admin. Server-local administrator privileges are not * specified in this document. */ -class GetWhoIsJob : public BaseJob { +class QUOTIENT_API GetWhoIsJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index fa4f475a..aa55d934 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -4,67 +4,64 @@ #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/v3", "/account/3pid")); } GetAccount3PIDsJob::GetAccount3PIDsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid") + makePath("/_matrix/client/v3", "/account/3pid")) {} Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), - QStringLiteral("/_matrix/client/r0") % "/account/3pid") + makePath("/_matrix/client/v3", "/account/3pid")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("three_pid_creds"), threePidCreds); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("three_pid_creds"), threePidCreds); + setRequestData({ _dataJson }); } 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/v3", "/account/3pid/add")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("sid"), sid); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret); + addParam<>(_dataJson, QStringLiteral("sid"), sid); + setRequestData({ _dataJson }); } 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/v3", "/account/3pid/bind")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("client_secret"), clientSecret); - addParam<>(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken); - addParam<>(_data, QStringLiteral("sid"), sid); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("client_secret"), clientSecret); + addParam<>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken); + addParam<>(_dataJson, QStringLiteral("sid"), sid); + setRequestData({ _dataJson }); } 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/v3", "/account/3pid/delete")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); addExpectedKey("id_server_unbind_result"); } @@ -72,32 +69,32 @@ 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/v3", "/account/3pid/unbind")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); addExpectedKey("id_server_unbind_result"); } RequestTokenTo3PIDEmailJob::RequestTokenTo3PIDEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid/email/requestToken", + makePath("/_matrix/client/v3", + "/account/3pid/email/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/3pid/msisdn/requestToken", + makePath("/_matrix/client/v3", + "/account/3pid/msisdn/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); } diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index e436971d..27334850 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -24,7 +24,7 @@ namespace Quotient { * Identifiers in this list may be used by the homeserver as, for example, * identifiers that it will accept to reset the user's account password. */ -class GetAccount3PIDsJob : public BaseJob { +class QUOTIENT_API GetAccount3PIDsJob : public BaseJob { public: // Inner data structures @@ -102,7 +102,7 @@ struct JsonObjectConverter<GetAccount3PIDsJob::ThirdPartyIdentifier> { * This results in this endpoint being an equivalent to `/3pid/bind` rather * than dual-purpose. */ -class Post3PIDsJob : public BaseJob { +class QUOTIENT_API Post3PIDsJob : public BaseJob { public: // Inner data structures @@ -128,6 +128,22 @@ public: * The third party credentials to associate with the account. */ explicit Post3PIDsJob(const ThreePidCredentials& threePidCreds); + + // Result properties + + /// An optional field containing a URL where the client must + /// submit the validation token to, with identical parameters + /// to the Identity Service API's `POST + /// /validate/email/submitToken` endpoint (without the requirement + /// for an access token). The homeserver must send this token to the + /// user (if applicable), who should then be prompted to provide it + /// to the client. + /// + /// If this field is not present, the client can assume that + /// verification will happen without the client's involvement + /// provided the homeserver advertises this specification version + /// in the `/versions` response (ie: r0.5.0). + QUrl submitUrl() const { return loadFromJson<QUrl>("submit_url"_ls); } }; template <> @@ -154,7 +170,7 @@ struct JsonObjectConverter<Post3PIDsJob::ThreePidCredentials> { * Homeservers should prevent the caller from adding a 3PID to their account if * it has already been added to another user's account on the homeserver. */ -class Add3PIDJob : public BaseJob { +class QUOTIENT_API Add3PIDJob : public BaseJob { public: /*! \brief Adds contact information to the user's account. * @@ -182,7 +198,7 @@ public: * * Homeservers should track successful binds so they can be unbound later. */ -class Bind3PIDJob : public BaseJob { +class QUOTIENT_API Bind3PIDJob : public BaseJob { public: /*! \brief Binds a 3PID to the user's account through an Identity Service. * @@ -211,7 +227,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class Delete3pidFromAccountJob : public BaseJob { +class QUOTIENT_API Delete3pidFromAccountJob : public BaseJob { public: /*! \brief Deletes a third party identifier from the user's account * @@ -235,7 +251,7 @@ public: /// 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` + /// 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. @@ -254,7 +270,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class Unbind3pidFromAccountJob : public BaseJob { +class QUOTIENT_API Unbind3pidFromAccountJob : public BaseJob { public: /*! \brief Removes a user's third party identifier from an identity server. * @@ -295,12 +311,12 @@ public: * 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`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * endpoint. The homeserver should validate * the email itself, either by sending a validation email itself or by using * a service it has control over. */ -class RequestTokenTo3PIDEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenTo3PIDEmailJob : public BaseJob { public: /*! \brief Begins the validation process for an email address for * association with the user's account. @@ -311,7 +327,7 @@ public: * 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`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * endpoint. The homeserver should validate * the email itself, either by sending a validation email itself or by * using a service it has control over. @@ -337,12 +353,12 @@ public: * 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`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * endpoint. The homeserver should validate * the phone number itself, either by sending a validation message itself or by * using a service it has control over. */ -class RequestTokenTo3PIDMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenTo3PIDMSISDNJob : public BaseJob { public: /*! \brief Begins the validation process for a phone number for association * with the user's account. @@ -353,7 +369,7 @@ public: * 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`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * 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. diff --git a/lib/csapi/appservice_room_directory.cpp b/lib/csapi/appservice_room_directory.cpp index e8ec55bf..dff7e032 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -4,18 +4,18 @@ #include "appservice_room_directory.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; -UpdateAppserviceRoomDirectoryVsibilityJob::UpdateAppserviceRoomDirectoryVsibilityJob( - const QString& networkId, const QString& roomId, const QString& visibility) +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/v3", "/directory/list/appservice/", + networkId, "/", roomId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("visibility"), visibility); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("visibility"), visibility); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/appservice_room_directory.h b/lib/csapi/appservice_room_directory.h index 2631f38c..d6268979 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -21,7 +21,7 @@ namespace Quotient { * 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 QUOTIENT_API 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..e04075b7 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -4,27 +4,26 @@ #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/v3", "/rooms/", roomId, "/ban")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } -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/v3", "/rooms/", roomId, "/unban")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/banning.h b/lib/csapi/banning.h index 48c054c2..e4c60ce3 100644 --- a/lib/csapi/banning.h +++ b/lib/csapi/banning.h @@ -18,7 +18,7 @@ namespace Quotient { * The caller must have the required power level in order to perform this * operation. */ -class BanJob : public BaseJob { +class QUOTIENT_API BanJob : public BaseJob { public: /*! \brief Ban a user in the room. * @@ -46,7 +46,7 @@ public: * The caller must have the required power level in order to perform this * operation. */ -class UnbanJob : public BaseJob { +class QUOTIENT_API UnbanJob : public BaseJob { public: /*! \brief Unban a user from the room. * @@ -55,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..ca2a543f 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/v3", "/capabilities")); } GetCapabilitiesJob::GetCapabilitiesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"), - QStringLiteral("/_matrix/client/r0") % "/capabilities") + makePath("/_matrix/client/v3", "/capabilities")) { addExpectedKey("capabilities"); } diff --git a/lib/csapi/capabilities.h b/lib/csapi/capabilities.h index da50c8c1..81b47cd4 100644 --- a/lib/csapi/capabilities.h +++ b/lib/csapi/capabilities.h @@ -13,7 +13,7 @@ namespace Quotient { * Gets information about the server's supported feature set * and other relevant capabilities. */ -class GetCapabilitiesJob : public BaseJob { +class QUOTIENT_API GetCapabilitiesJob : public BaseJob { public: // Inner data structures diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 7ae89739..6f6738af 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/v3", "/upload"), queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); - setRequestData(Data(content)); + setRequestData({ 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/v3", "/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/v3", "/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/v3", "/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/v3", "/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,55 +101,54 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName % "/" - % mediaId, + makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/", mediaId), queryToGetContentThumbnail(width, height, method, allowRemote)); } GetContentThumbnailJob::GetContentThumbnailJob(const QString& serverName, - const QString& mediaId, int width, - int height, const QString& method, + const QString& mediaId, + int width, int height, + const QString& method, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentThumbnailJob"), - QStringLiteral("/_matrix/media/r0") % "/thumbnail/" % serverName - % "/" % mediaId, + makePath("/_matrix/media/v3", "/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/v3", + "/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/v3", "/preview_url"), queryToGetUrlPreview(url, ts)) {} QUrl GetConfigJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/media/r0") - % "/config"); + makePath("/_matrix/media/v3", "/config")); } GetConfigJob::GetConfigJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"), - QStringLiteral("/_matrix/media/r0") % "/config") + makePath("/_matrix/media/v3", "/config")) {} diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index f3d7309a..2ba66a35 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -14,7 +14,7 @@ namespace Quotient { /*! \brief Upload some content to the content repository. * */ -class UploadContentJob : public BaseJob { +class QUOTIENT_API UploadContentJob : public BaseJob { public: /*! \brief Upload some content to the content repository. * @@ -34,16 +34,13 @@ public: /// The [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the /// uploaded content. - QString contentUri() const - { - return loadFromJson<QString>("content_uri"_ls); - } + QUrl contentUri() const { return loadFromJson<QUrl>("content_uri"_ls); } }; /*! \brief Download content from the content repository. * */ -class GetContentJob : public BaseJob { +class QUOTIENT_API GetContentJob : public BaseJob { public: /*! \brief Download content from the content repository. * @@ -90,7 +87,7 @@ public: * the previous endpoint) but replace the target file name with the one * provided by the caller. */ -class GetContentOverrideNameJob : public BaseJob { +class QUOTIENT_API GetContentOverrideNameJob : public BaseJob { public: /*! \brief Download content from the content repository overriding the file * name @@ -145,7 +142,7 @@ public: * See the [Thumbnails](/client-server-api/#thumbnails) section for more * information. */ -class GetContentThumbnailJob : public BaseJob { +class QUOTIENT_API GetContentThumbnailJob : public BaseJob { public: /*! \brief Download a thumbnail of content from the content repository * @@ -165,7 +162,8 @@ public: * * \param method * The desired resizing method. See the - * [Thumbnails](/client-server-api/#thumbnails) section for more information. + * [Thumbnails](/client-server-api/#thumbnails) section for more + * information. * * \param allowRemote * Indicates to the server that it should not attempt to fetch @@ -207,7 +205,7 @@ public: * 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 { +class QUOTIENT_API GetUrlPreviewJob : public BaseJob { public: /*! \brief Get information about a URL for a client * @@ -219,14 +217,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 @@ -239,7 +237,7 @@ public: /// An [MXC URI](/client-server-api/#matrix-content-mxc-uris) to the image. /// Omitted if there is no image. - QString ogImage() const { return loadFromJson<QString>("og:image"_ls); } + QUrl ogImage() const { return loadFromJson<QUrl>("og:image"_ls); } }; /*! \brief Get the configuration for the content repository. @@ -255,7 +253,7 @@ public: * content repository APIs, for example, proxies may enforce a lower upload size * limit than is advertised by the server on this endpoint. */ -class GetConfigJob : public BaseJob { +class QUOTIENT_API GetConfigJob : public BaseJob { public: /// Get the configuration for the content repository. explicit GetConfigJob(); diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index a94f9951..afae80af 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,24 +16,26 @@ 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/v3", "/createRoom")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); - addParam<IfNotEmpty>(_data, QStringLiteral("room_alias_name"), + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("visibility"), visibility); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("room_alias_name"), roomAliasName); - addParam<IfNotEmpty>(_data, QStringLiteral("name"), name); - addParam<IfNotEmpty>(_data, QStringLiteral("topic"), topic); - addParam<IfNotEmpty>(_data, QStringLiteral("invite"), invite); - addParam<IfNotEmpty>(_data, QStringLiteral("invite_3pid"), invite3pid); - addParam<IfNotEmpty>(_data, QStringLiteral("room_version"), roomVersion); - addParam<IfNotEmpty>(_data, QStringLiteral("creation_content"), + addParam<IfNotEmpty>(_dataJson, QStringLiteral("name"), name); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("topic"), topic); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("invite"), invite); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("invite_3pid"), invite3pid); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("room_version"), roomVersion); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("creation_content"), creationContent); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_state"), initialState); - addParam<IfNotEmpty>(_data, QStringLiteral("preset"), preset); - addParam<IfNotEmpty>(_data, QStringLiteral("is_direct"), isDirect); - addParam<IfNotEmpty>(_data, QStringLiteral("power_level_content_override"), + addParam<IfNotEmpty>(_dataJson, QStringLiteral("initial_state"), + initialState); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("preset"), preset); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("is_direct"), isDirect); + addParam<IfNotEmpty>(_dataJson, + QStringLiteral("power_level_content_override"), powerLevelContentOverride); - setRequestData(std::move(_data)); + setRequestData({ _dataJson }); addExpectedKey("room_id"); } diff --git a/lib/csapi/create_room.h b/lib/csapi/create_room.h index 81dfbffc..336b9767 100644 --- a/lib/csapi/create_room.h +++ b/lib/csapi/create_room.h @@ -26,16 +26,18 @@ namespace Quotient { * (and not other members) permission to send state events. Overridden * by the `power_level_content_override` parameter. * - * 4. Events set by the `preset`. Currently these are the `m.room.join_rules`, + * 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + * + * 5. 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 + * 6. 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` + * 7. 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 + * 8. 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: @@ -53,7 +55,7 @@ namespace Quotient { * requesting user as the creator, alongside other keys provided in the * `creation_content`. */ -class CreateRoomJob : public BaseJob { +class QUOTIENT_API CreateRoomJob : public BaseJob { public: // Inner data structures @@ -73,17 +75,20 @@ public: /// (and not other members) permission to send state events. Overridden /// by the `power_level_content_override` parameter. /// - /// 4. Events set by the `preset`. Currently these are the + /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + /// + /// 5. 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 + /// 6. 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` + /// 7. 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` + /// 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member` /// with /// `membership: invite` and `m.room.third_party_invite`). /// @@ -132,17 +137,20 @@ public: /// (and not other members) permission to send state events. Overridden /// by the `power_level_content_override` parameter. /// - /// 4. Events set by the `preset`. Currently these are the + /// 4. An `m.room.canonical_alias` event if `room_alias_name` is given. + /// + /// 5. 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 + /// 6. 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` + /// 7. 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` + /// 8. Invite events implied by `invite` and `invite_3pid` (`m.room.member` /// with /// `membership: invite` and `m.room.third_party_invite`). /// @@ -190,7 +198,8 @@ public: * would be `#foo:example.com`. * * The complete room alias will become the canonical alias for - * the room. + * the room and an `m.room.canonical_alias` event will be sent + * into the room. * * \param name * If this is included, an `m.room.name` event will be sent @@ -218,9 +227,10 @@ public: * * \param creationContent * 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. + * of the [`m.room.create`](/client-server-api/#mroomcreate) event. The + * server will overwrite the following keys: `creator`, `room_version`. + * Future versions of the specification may allow the server to overwrite + * other keys. * * \param initialState * A list of state events to set in the new room. This allows @@ -229,7 +239,7 @@ public: * with type, state_key and content keys set. * * Takes precedence over events set by `preset`, but gets - * overriden by `name` and `topic` keys. + * overridden by `name` and `topic` keys. * * \param preset * Convenience parameter for setting various default state events @@ -249,7 +259,7 @@ public: * \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`](client-server-api/#mroompower_levels) + * [`m.room.power_levels`](/client-server-api/#mroompower_levels) * event content prior to it being sent to the room. Defaults to * overriding nothing. */ diff --git a/lib/csapi/cross_signing.cpp b/lib/csapi/cross_signing.cpp new file mode 100644 index 00000000..83136d71 --- /dev/null +++ b/lib/csapi/cross_signing.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * 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, + const Omittable<AuthenticationData>& auth) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), + makePath("/_matrix/client/v3", "/keys/device_signing/upload")) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("master_key"), masterKey); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("self_signing_key"), + selfSigningKey); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("user_signing_key"), + userSigningKey); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); +} + +UploadCrossSigningSignaturesJob::UploadCrossSigningSignaturesJob( + const QHash<QString, QHash<QString, QJsonObject>>& signatures) + : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningSignaturesJob"), + makePath("/_matrix/client/v3", "/keys/signatures/upload")) +{ + setRequestData({ toJson(signatures) }); +} diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h new file mode 100644 index 00000000..6cea73e6 --- /dev/null +++ b/lib/csapi/cross_signing.h @@ -0,0 +1,78 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "csapi/definitions/auth_data.h" +#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 QUOTIENT_API 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. + * + * \param auth + * Additional authentication information for the + * user-interactive authentication API. + */ + explicit UploadCrossSigningKeysJob( + const Omittable<CrossSigningKey>& masterKey = none, + const Omittable<CrossSigningKey>& selfSigningKey = none, + const Omittable<CrossSigningKey>& userSigningKey = none, + const Omittable<AuthenticationData>& auth = 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 QUOTIENT_API 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/auth_data.h b/lib/csapi/definitions/auth_data.h index e92596d0..a9972323 100644 --- a/lib/csapi/definitions/auth_data.h +++ b/lib/csapi/definitions/auth_data.h @@ -10,7 +10,10 @@ namespace Quotient { /// Used by clients to submit authentication information to the /// interactive-authentication API struct AuthenticationData { - /// The login type that the client is attempting to complete. + /// The authentication type that the client is attempting to complete. + /// May be omitted if `session` is given, and the client is reissuing a + /// request which it believes has been completed out-of-band (for example, + /// via the [fallback mechanism](#fallback)). QString type; /// The value of the session key given by the homeserver. @@ -25,7 +28,7 @@ struct JsonObjectConverter<AuthenticationData> { static void dumpTo(QJsonObject& jo, const AuthenticationData& pod) { fillJson(jo, pod.authInfo); - addParam<>(jo, QStringLiteral("type"), pod.type); + addParam<IfNotEmpty>(jo, QStringLiteral("type"), pod.type); addParam<IfNotEmpty>(jo, QStringLiteral("session"), pod.session); } static void fillFrom(QJsonObject jo, AuthenticationData& pod) 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/openid_token.h b/lib/csapi/definitions/openid_token.h index 3c447321..9b026dea 100644 --- a/lib/csapi/definitions/openid_token.h +++ b/lib/csapi/definitions/openid_token.h @@ -8,7 +8,7 @@ namespace Quotient { -struct OpenidToken { +struct OpenIdCredentials { /// 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. @@ -27,8 +27,8 @@ struct OpenidToken { }; template <> -struct JsonObjectConverter<OpenidToken> { - static void dumpTo(QJsonObject& jo, const OpenidToken& pod) +struct JsonObjectConverter<OpenIdCredentials> { + static void dumpTo(QJsonObject& jo, const OpenIdCredentials& pod) { addParam<>(jo, QStringLiteral("access_token"), pod.accessToken); addParam<>(jo, QStringLiteral("token_type"), pod.tokenType); @@ -36,7 +36,7 @@ struct JsonObjectConverter<OpenidToken> { pod.matrixServerName); addParam<>(jo, QStringLiteral("expires_in"), pod.expiresIn); } - static void fillFrom(const QJsonObject& jo, OpenidToken& pod) + static void fillFrom(const QJsonObject& jo, OpenIdCredentials& pod) { fromJson(jo.value("access_token"_ls), pod.accessToken); fromJson(jo.value("token_type"_ls), pod.tokenType); diff --git a/lib/csapi/definitions/public_rooms_response.h b/lib/csapi/definitions/public_rooms_response.h index 8f30e607..7c7d9cc6 100644 --- a/lib/csapi/definitions/public_rooms_response.h +++ b/lib/csapi/definitions/public_rooms_response.h @@ -9,9 +9,6 @@ namespace Quotient { struct PublicRoomsChunk { - /// Aliases of the room. May be empty. - QStringList aliases; - /// The canonical alias of the room, if any. QString canonicalAlias; @@ -36,14 +33,23 @@ struct PublicRoomsChunk { bool guestCanJoin; /// The URL for the room's avatar, if one is set. - QString avatarUrl; + QUrl avatarUrl; + + /// The `type` of room (from + /// [`m.room.create`](/client-server-api/#mroomcreate)), if any. + QString roomType; + + /// 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 <> struct JsonObjectConverter<PublicRoomsChunk> { static void dumpTo(QJsonObject& jo, const PublicRoomsChunk& pod) { - addParam<IfNotEmpty>(jo, QStringLiteral("aliases"), pod.aliases); addParam<IfNotEmpty>(jo, QStringLiteral("canonical_alias"), pod.canonicalAlias); addParam<IfNotEmpty>(jo, QStringLiteral("name"), pod.name); @@ -54,10 +60,11 @@ 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("room_type"), pod.roomType); + addParam<IfNotEmpty>(jo, QStringLiteral("join_rule"), pod.joinRule); } static void fillFrom(const QJsonObject& jo, PublicRoomsChunk& pod) { - fromJson(jo.value("aliases"_ls), pod.aliases); fromJson(jo.value("canonical_alias"_ls), pod.canonicalAlias); fromJson(jo.value("name"_ls), pod.name); fromJson(jo.value("num_joined_members"_ls), pod.numJoinedMembers); @@ -66,46 +73,8 @@ 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); - } -}; - -/// A list of the rooms on the server. -struct PublicRoomsResponse { - /// A paginated chunk of public rooms. - QVector<PublicRoomsChunk> chunk; - - /// A pagination token for the response. The absence of this token - /// means there are no more results to fetch and the client should - /// stop paginating. - QString nextBatch; - - /// A pagination token that allows fetching previous results. The - /// absence of this token means there are no results before this - /// batch, i.e. this is the first batch. - QString prevBatch; - - /// An estimate on the total number of public rooms, if the - /// server has an estimate. - Omittable<int> totalRoomCountEstimate; -}; - -template <> -struct JsonObjectConverter<PublicRoomsResponse> { - static void dumpTo(QJsonObject& jo, const PublicRoomsResponse& pod) - { - addParam<>(jo, QStringLiteral("chunk"), pod.chunk); - addParam<IfNotEmpty>(jo, QStringLiteral("next_batch"), pod.nextBatch); - addParam<IfNotEmpty>(jo, QStringLiteral("prev_batch"), pod.prevBatch); - addParam<IfNotEmpty>(jo, QStringLiteral("total_room_count_estimate"), - pod.totalRoomCountEstimate); - } - static void fillFrom(const QJsonObject& jo, PublicRoomsResponse& pod) - { - fromJson(jo.value("chunk"_ls), pod.chunk); - fromJson(jo.value("next_batch"_ls), pod.nextBatch); - fromJson(jo.value("prev_batch"_ls), pod.prevBatch); - fromJson(jo.value("total_room_count_estimate"_ls), - pod.totalRoomCountEstimate); + fromJson(jo.value("room_type"_ls), pod.roomType); + 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 ce66d075..6a048ba8 100644 --- a/lib/csapi/definitions/push_condition.h +++ b/lib/csapi/definitions/push_condition.h @@ -24,9 +24,7 @@ struct PushCondition { QString key; /// Required for `event_match` conditions. The glob-style pattern to - /// match against. Patterns with no special glob characters should be - /// treated as having asterisks prepended and appended when testing the - /// condition. + /// match against. QString pattern; /// Required for `room_member_count` conditions. A decimal integer diff --git a/lib/csapi/definitions/request_token_response.h b/lib/csapi/definitions/request_token_response.h index f9981100..d5fbbadb 100644 --- a/lib/csapi/definitions/request_token_response.h +++ b/lib/csapi/definitions/request_token_response.h @@ -25,7 +25,7 @@ struct RequestTokenResponse { /// will happen without the client's involvement provided the homeserver /// 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 91caf667..293e5492 100644 --- a/lib/csapi/definitions/room_event_filter.h +++ b/lib/csapi/definitions/room_event_filter.h @@ -11,6 +11,11 @@ namespace Quotient { struct RoomEventFilter : EventFilter { + /// If `true`, enables per-[thread](/client-server-api/#threading) + /// notification counts. Only applies to the `/sync` endpoint. Defaults to + /// `false`. + Omittable<bool> unreadThreadNotifications; + /// If `true`, enables lazy-loading of membership events. See /// [Lazy-loading room /// members](/client-server-api/#lazy-loading-room-members) for more @@ -44,6 +49,8 @@ struct JsonObjectConverter<RoomEventFilter> { static void dumpTo(QJsonObject& jo, const RoomEventFilter& pod) { fillJson<EventFilter>(jo, pod); + addParam<IfNotEmpty>(jo, QStringLiteral("unread_thread_notifications"), + pod.unreadThreadNotifications); addParam<IfNotEmpty>(jo, QStringLiteral("lazy_load_members"), pod.lazyLoadMembers); addParam<IfNotEmpty>(jo, QStringLiteral("include_redundant_members"), @@ -56,6 +63,8 @@ struct JsonObjectConverter<RoomEventFilter> { static void fillFrom(const QJsonObject& jo, RoomEventFilter& pod) { fillFromJson<EventFilter>(jo, pod); + fromJson(jo.value("unread_thread_notifications"_ls), + pod.unreadThreadNotifications); fromJson(jo.value("lazy_load_members"_ls), pod.lazyLoadMembers); fromJson(jo.value("include_redundant_members"_ls), pod.includeRedundantMembers); 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..6f2badee 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -4,61 +4,58 @@ #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/v3", "/devices")); } GetDevicesJob::GetDevicesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"), - QStringLiteral("/_matrix/client/r0") % "/devices") + makePath("/_matrix/client/v3", "/devices")) {} QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/devices/" % deviceId); + makePath("/_matrix/client/v3", "/devices/", + deviceId)); } GetDeviceJob::GetDeviceJob(const QString& deviceId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/v3", "/devices/", deviceId)) {} UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/v3", "/devices/", deviceId)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("display_name"), displayName); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("display_name"), displayName); + setRequestData({ _dataJson }); } DeleteDeviceJob::DeleteDeviceJob(const QString& deviceId, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteDeviceJob"), - QStringLiteral("/_matrix/client/r0") % "/devices/" % deviceId) + makePath("/_matrix/client/v3", "/devices/", deviceId)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } DeleteDevicesJob::DeleteDevicesJob(const QStringList& devices, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Post, QStringLiteral("DeleteDevicesJob"), - QStringLiteral("/_matrix/client/r0") % "/delete_devices") + makePath("/_matrix/client/v3", "/delete_devices")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("devices"), devices); - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("devices"), devices); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/device_management.h b/lib/csapi/device_management.h index 7fb69873..c10389b3 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -15,7 +15,7 @@ namespace Quotient { * * Gets information about all devices for the current user. */ -class GetDevicesJob : public BaseJob { +class QUOTIENT_API GetDevicesJob : public BaseJob { public: /// List registered devices for the current user explicit GetDevicesJob(); @@ -40,7 +40,7 @@ public: * * Gets information on a single device, by device id. */ -class GetDeviceJob : public BaseJob { +class QUOTIENT_API GetDeviceJob : public BaseJob { public: /*! \brief Get a single device * @@ -66,7 +66,7 @@ public: * * Updates the metadata on the given device. */ -class UpdateDeviceJob : public BaseJob { +class QUOTIENT_API UpdateDeviceJob : public BaseJob { public: /*! \brief Update a device * @@ -86,9 +86,10 @@ public: * 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. + * Deletes the given device, and invalidates any access token associated with + * it. */ -class DeleteDeviceJob : public BaseJob { +class QUOTIENT_API DeleteDeviceJob : public BaseJob { public: /*! \brief Delete a device * @@ -111,7 +112,7 @@ public: * Deletes the given devices, and invalidates any access token associated with * them. */ -class DeleteDevicesJob : public BaseJob { +class QUOTIENT_API DeleteDevicesJob : public BaseJob { public: /*! \brief Bulk deletion of devices * diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index 25ea82e2..c1255bb1 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -4,58 +4,52 @@ #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/v3", "/directory/room/", roomAlias)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("room_id"), roomId); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("room_id"), roomId); + setRequestData({ _dataJson }); } QUrl GetRoomIdByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& roomAlias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/directory/room/" % roomAlias); + makePath("/_matrix/client/v3", + "/directory/room/", roomAlias)); } GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias, + makePath("/_matrix/client/v3", "/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/v3", + "/directory/room/", roomAlias)); } DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/directory/room/" - % roomAlias) + makePath("/_matrix/client/v3", "/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/v3", "/rooms/", + roomId, "/aliases")); } GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/aliases") + makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases")) { addExpectedKey("aliases"); } diff --git a/lib/csapi/directory.h b/lib/csapi/directory.h index 93a31595..0bd13a76 100644 --- a/lib/csapi/directory.h +++ b/lib/csapi/directory.h @@ -11,7 +11,7 @@ namespace Quotient { /*! \brief Create a new mapping from room alias to room ID. * */ -class SetRoomAliasJob : public BaseJob { +class QUOTIENT_API SetRoomAliasJob : public BaseJob { public: /*! \brief Create a new mapping from room alias to room ID. * @@ -32,7 +32,7 @@ public: * domain part of the alias does not correspond to the server's own * domain. */ -class GetRoomIdByAliasJob : public BaseJob { +class QUOTIENT_API GetRoomIdByAliasJob : public BaseJob { public: /*! \brief Get the room ID corresponding to this room alias. * @@ -76,7 +76,7 @@ public: * return a successful response even if the user does not have permission to * update the `m.room.canonical_alias` event. */ -class DeleteRoomAliasJob : public BaseJob { +class QUOTIENT_API DeleteRoomAliasJob : public BaseJob { public: /*! \brief Remove a mapping of room alias to room ID. * @@ -112,7 +112,7 @@ public: * as they are not curated, unlike those listed in the `m.room.canonical_alias` * state event. */ -class GetLocalAliasesJob : public BaseJob { +class QUOTIENT_API GetLocalAliasesJob : public BaseJob { public: /*! \brief Get a list of local aliases on a given room. * diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index d2a5f522..4ebbbf98 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/v3", "/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/v3", "/rooms/", roomId, "/context/", + eventId), queryToGetEventContext(limit, filter)) {} diff --git a/lib/csapi/event_context.h b/lib/csapi/event_context.h index 4e50edf3..1614c7ed 100644 --- a/lib/csapi/event_context.h +++ b/lib/csapi/event_context.h @@ -4,7 +4,8 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" namespace Quotient { @@ -19,7 +20,7 @@ namespace Quotient { * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) * for more information. */ -class GetEventContextJob : public BaseJob { +class QUOTIENT_API GetEventContextJob : public BaseJob { public: /*! \brief Get events and state around the specified event. * diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index bb3a893f..2469fbd1 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/v3", "/user/", userId, "/filter")) { - setRequestData(Data(toJson(filter))); + setRequestData({ 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/v3", "/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/v3", "/user/", userId, "/filter/", + filterId)) {} diff --git a/lib/csapi/filter.h b/lib/csapi/filter.h index 01bec36b..9518a461 100644 --- a/lib/csapi/filter.h +++ b/lib/csapi/filter.h @@ -16,7 +16,7 @@ namespace Quotient { * Returns a filter ID that may be used in future requests to * restrict which events are returned to the client. */ -class DefineFilterJob : public BaseJob { +class QUOTIENT_API DefineFilterJob : public BaseJob { public: /*! \brief Upload a new filter. * @@ -41,7 +41,7 @@ public: /*! \brief Download a filter * */ -class GetFilterJob : public BaseJob { +class QUOTIENT_API GetFilterJob : public BaseJob { public: /*! \brief Download a filter * diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 01620f9e..41a8b5be 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/v3", "/rooms/", roomId, "/invite")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/inviting.h b/lib/csapi/inviting.h index 1e65ecff..cb9d052b 100644 --- a/lib/csapi/inviting.h +++ b/lib/csapi/inviting.h @@ -14,7 +14,7 @@ namespace Quotient { * 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](/client-server-api/#post_matrixclientr0roomsroomidinvite-1). + * section](/client-server-api/#post_matrixclientv3roomsroomidinvite-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 @@ -26,7 +26,7 @@ namespace Quotient { * If the user was invited to the room, the homeserver will append a * `m.room.member` event to the room. */ -class InviteUserJob : public BaseJob { +class QUOTIENT_API InviteUserJob : public BaseJob { public: /*! \brief Invite a user to participate in a particular room. * @@ -35,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..cdba95e9 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/v3", "/rooms/", roomId, "/join")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_signed"), thirdPartySigned); - setRequestData(std::move(_data)); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); 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/v3", "/join/", roomIdOrAlias), queryToJoinRoom(serverName)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_signed"), + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_signed"), thirdPartySigned); - setRequestData(std::move(_data)); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); addExpectedKey("room_id"); } diff --git a/lib/csapi/joining.h b/lib/csapi/joining.h index 1b6f99e4..c86baa90 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -22,10 +22,10 @@ namespace Quotient { * * After a user has joined a room, the room will appear as an entry in the * response of the - * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and - * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. + * [`/initialSync`](/client-server-api/#get_matrixclientv3initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs. */ -class JoinRoomByIdJob : public BaseJob { +class QUOTIENT_API JoinRoomByIdJob : public BaseJob { public: /*! \brief Start the requesting user participating in a particular room. * @@ -36,10 +36,15 @@ public: * If supplied, the homeserver must verify that it matches a pending * `m.room.third_party_invite` event in the room, and perform * key validity checking if required by the event. + * + * \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 @@ -50,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`. + * `/rooms/{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 @@ -59,10 +64,10 @@ public: * * After a user has joined a room, the room will appear as an entry in the * response of the - * [`/initialSync`](/client-server-api/#get_matrixclientr0initialsync) and - * [`/sync`](/client-server-api/#get_matrixclientr0sync) APIs. + * [`/initialSync`](/client-server-api/#get_matrixclientv3initialsync) and + * [`/sync`](/client-server-api/#get_matrixclientv3sync) APIs. */ -class JoinRoomJob : public BaseJob { +class QUOTIENT_API JoinRoomJob : public BaseJob { public: /*! \brief Start the requesting user participating in a particular room. * @@ -77,10 +82,15 @@ public: * 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..2e4978f2 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -4,50 +4,52 @@ #include "keys.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; UploadKeysJob::UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys, - const QHash<QString, QVariant>& oneTimeKeys) + const OneTimeKeys& oneTimeKeys, + const OneTimeKeys& fallbackKeys) : BaseJob(HttpVerb::Post, QStringLiteral("UploadKeysJob"), - QStringLiteral("/_matrix/client/r0") % "/keys/upload") + makePath("/_matrix/client/v3", "/keys/upload")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("device_keys"), deviceKeys); - addParam<IfNotEmpty>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_keys"), deviceKeys); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("one_time_keys"), + oneTimeKeys); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("fallback_keys"), + fallbackKeys); + setRequestData({ _dataJson }); addExpectedKey("one_time_key_counts"); } 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/v3", "/keys/query")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); - addParam<>(_data, QStringLiteral("device_keys"), deviceKeys); - addParam<IfNotEmpty>(_data, QStringLiteral("token"), token); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout); + addParam<>(_dataJson, QStringLiteral("device_keys"), deviceKeys); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("token"), token); + setRequestData({ _dataJson }); } 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/v3", "/keys/claim")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); - addParam<>(_data, QStringLiteral("one_time_keys"), oneTimeKeys); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout); + addParam<>(_dataJson, QStringLiteral("one_time_keys"), oneTimeKeys); + setRequestData({ _dataJson }); addExpectedKey("one_time_keys"); } 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 +59,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/v3", + "/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/v3", "/keys/changes"), queryToGetKeysChanges(from, to)) {} diff --git a/lib/csapi/keys.h b/lib/csapi/keys.h index 621945eb..b28de305 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,8 +4,11 @@ #pragma once +#include "csapi/definitions/cross_signing_key.h" #include "csapi/definitions/device_keys.h" +#include "e2ee/e2ee.h" + #include "jobs/basejob.h" namespace Quotient { @@ -14,7 +17,7 @@ namespace Quotient { * * Publishes end-to-end encryption keys for the device. */ -class UploadKeysJob : public BaseJob { +class QUOTIENT_API UploadKeysJob : public BaseJob { public: /*! \brief Upload end-to-end encryption keys. * @@ -29,14 +32,32 @@ public: * by the [key algorithm](/client-server-api/#key-algorithms). * * May be absent if no new one-time keys are required. + * + * \param fallbackKeys + * The public key which should be used if the device's one-time keys + * are exhausted. The fallback key is not deleted once used, but should + * be replaced when additional one-time keys are being uploaded. The + * server will notify the client of the fallback key being used through + * `/sync`. + * + * There can only be at most one key per algorithm uploaded, and the + * server will only persist one key per algorithm. + * + * When uploading a signed key, an additional `fallback: true` key should + * be included to denote that the key is a fallback key. + * + * May be absent if a new fallback key is not required. */ explicit UploadKeysJob(const Omittable<DeviceKeys>& deviceKeys = none, - const QHash<QString, QVariant>& oneTimeKeys = {}); + const OneTimeKeys& oneTimeKeys = {}, + const OneTimeKeys& fallbackKeys = {}); // Result properties /// For each key algorithm, the number of unclaimed one-time keys /// of that type currently held on the server for this device. + /// If an algorithm is not listed, the count for that algorithm + /// is to be assumed zero. QHash<QString, int> oneTimeKeyCounts() const { return loadFromJson<QHash<QString, int>>("one_time_key_counts"_ls); @@ -47,7 +68,7 @@ public: * * Returns the current devices and identity keys for the given users. */ -class QueryKeysJob : public BaseJob { +class QUOTIENT_API QueryKeysJob : public BaseJob { public: // Inner data structures @@ -114,6 +135,38 @@ public: 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 <> @@ -139,7 +192,7 @@ struct JsonObjectConverter<QueryKeysJob::DeviceInformation> { * * Claims one-time keys for use in pre-key messages. */ -class ClaimKeysJob : public BaseJob { +class QUOTIENT_API ClaimKeysJob : public BaseJob { public: /*! \brief Claim one-time encryption keys. * @@ -174,9 +227,12 @@ public: /// /// See the [key algorithms](/client-server-api/#key-algorithms) section for /// information on the Key Object format. - QHash<QString, QHash<QString, QVariant>> oneTimeKeys() const + /// + /// If necessary, the claimed key might be a fallback key. Fallback + /// keys are re-used by the server until replaced by the device. + QHash<QString, QHash<QString, OneTimeKeys>> oneTimeKeys() const { - return loadFromJson<QHash<QString, QHash<QString, QVariant>>>( + return loadFromJson<QHash<QString, QHash<QString, OneTimeKeys>>>( "one_time_keys"_ls); } }; @@ -193,14 +249,14 @@ public: * * added new device identity keys or removed an existing device with * identity keys, between `from` and `to`. */ -class GetKeysChangesJob : public BaseJob { +class QUOTIENT_API 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`](/client-server-api/#get_matrixclientr0sync). Users who have not + * [`/sync`](/client-server-api/#get_matrixclientv3sync). 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. @@ -208,7 +264,7 @@ public: * \param to * 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 + * [`/sync`](/client-server-api/#get_matrixclientv3sync) - typically the * most recent such call. This may be used by the server as a hint to check * its caches are up to date. */ diff --git a/lib/csapi/kicking.cpp b/lib/csapi/kicking.cpp index 7de5ce01..4ca39c4c 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -4,17 +4,15 @@ #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/v3", "/rooms/", roomId, "/kick")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("user_id"), userId); - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("user_id"), userId); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/kicking.h b/lib/csapi/kicking.h index 11018368..6ac106e2 100644 --- a/lib/csapi/kicking.h +++ b/lib/csapi/kicking.h @@ -20,7 +20,7 @@ namespace Quotient { * directly adjust the target member's state by making a request to * `/rooms/<room id>/state/m.room.member/<user id>`. */ -class KickJob : public BaseJob { +class QUOTIENT_API KickJob : public BaseJob { public: /*! \brief Kick a user from the room. * diff --git a/lib/csapi/knocking.cpp b/lib/csapi/knocking.cpp new file mode 100644 index 00000000..b9da4b9b --- /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/v3", "/knock/", roomIdOrAlias), + queryToKnockRoom(serverName)) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); + addExpectedKey("room_id"); +} diff --git a/lib/csapi/knocking.h b/lib/csapi/knocking.h new file mode 100644 index 00000000..f43033a8 --- /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_matrixclientv3sync) API. + */ +class QUOTIENT_API 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..ba91f26a 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/v3", "/rooms/", roomId, "/leave")) { - return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/rooms/" % roomId % "/leave"); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } -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/v3", "/rooms/", + roomId, "/forget")); } ForgetRoomJob::ForgetRoomJob(const QString& roomId) : BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/forget") + makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget")) {} diff --git a/lib/csapi/leaving.h b/lib/csapi/leaving.h index 1bea7e41..19cac3f0 100644 --- a/lib/csapi/leaving.h +++ b/lib/csapi/leaving.h @@ -22,21 +22,18 @@ namespace Quotient { * The user will still be allowed to retrieve history from the room which * they were previously allowed to see. */ -class LeaveRoomJob : public BaseJob { +class QUOTIENT_API LeaveRoomJob : public BaseJob { public: /*! \brief Stop the requesting user participating in a particular room. * * \param roomId * The room identifier to leave. - */ - explicit LeaveRoomJob(const QString& roomId); - - /*! \brief Construct a URL without creating a full-fledged job object * - * This function can be used when a URL for LeaveRoomJob - * is necessary but the job itself isn't. + * \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. @@ -51,7 +48,7 @@ public: * If the user is currently joined to the room, they must leave the room * before calling this API. */ -class ForgetRoomJob : public BaseJob { +class QUOTIENT_API ForgetRoomJob : public BaseJob { public: /*! \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..cdcf3eb2 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/v3", "/joined_rooms")); } GetJoinedRoomsJob::GetJoinedRoomsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/joined_rooms") + makePath("/_matrix/client/v3", "/joined_rooms")) { addExpectedKey("joined_rooms"); } diff --git a/lib/csapi/list_joined_rooms.h b/lib/csapi/list_joined_rooms.h index 59a24a49..aea68afd 100644 --- a/lib/csapi/list_joined_rooms.h +++ b/lib/csapi/list_joined_rooms.h @@ -12,7 +12,7 @@ namespace Quotient { * * This API returns a list of the user's current rooms. */ -class GetJoinedRoomsJob : public BaseJob { +class QUOTIENT_API GetJoinedRoomsJob : public BaseJob { public: /// Lists the user's current rooms. explicit GetJoinedRoomsJob(); diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 415d816c..4deecfc2 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -4,41 +4,37 @@ #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/v3", + "/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/v3", "/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/v3", "/directory/list/room/", roomId)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("visibility"), visibility); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("visibility"), visibility); + setRequestData({ _dataJson }); } 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/v3", + "/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/v3", "/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,17 +74,17 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<bool> includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"), - QStringLiteral("/_matrix/client/r0") % "/publicRooms", + makePath("/_matrix/client/v3", "/publicRooms"), queryToQueryPublicRooms(server)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit); - addParam<IfNotEmpty>(_data, QStringLiteral("since"), since); - addParam<IfNotEmpty>(_data, QStringLiteral("filter"), filter); - addParam<IfNotEmpty>(_data, QStringLiteral("include_all_networks"), + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("since"), since); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("filter"), filter); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("include_all_networks"), includeAllNetworks); - addParam<IfNotEmpty>(_data, QStringLiteral("third_party_instance_id"), + addParam<IfNotEmpty>(_dataJson, QStringLiteral("third_party_instance_id"), thirdPartyInstanceId); - setRequestData(std::move(_data)); + setRequestData({ _dataJson }); addExpectedKey("chunk"); } diff --git a/lib/csapi/list_public_rooms.h b/lib/csapi/list_public_rooms.h index 963c8b56..3b6b91b9 100644 --- a/lib/csapi/list_public_rooms.h +++ b/lib/csapi/list_public_rooms.h @@ -14,7 +14,7 @@ namespace Quotient { * * Gets the visibility of a given room on the server's public room directory. */ -class GetRoomVisibilityOnDirectoryJob : public BaseJob { +class QUOTIENT_API GetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! \brief Gets the visibility of a room in the directory * @@ -48,7 +48,7 @@ public: * here, for instance that room visibility can only be changed by * the room creator or a server administrator. */ -class SetRoomVisibilityOnDirectoryJob : public BaseJob { +class QUOTIENT_API SetRoomVisibilityOnDirectoryJob : public BaseJob { public: /*! \brief Sets the visibility of a room in the room directory * @@ -70,7 +70,7 @@ public: * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ -class GetPublicRoomsJob : public BaseJob { +class QUOTIENT_API GetPublicRoomsJob : public BaseJob { public: /*! \brief Lists the public rooms on the server. * @@ -133,15 +133,20 @@ public: * This API returns paginated responses. The rooms are ordered by the number * of joined members, with the largest rooms first. */ -class QueryPublicRoomsJob : public BaseJob { +class QUOTIENT_API QueryPublicRoomsJob : public BaseJob { public: // Inner data structures /// Filter to apply to the results. struct Filter { - /// A string to search for in the room metadata, e.g. name, - /// topic, canonical alias etc. (Optional). + /// An optional string to search for in the room metadata, e.g. name, + /// topic, canonical alias, etc. QString genericSearchTerm; + /// An optional list of [room types](/client-server-api/#types) to + /// search for. To include rooms without a room type, specify `null` + /// within this list. When not specified, all applicable rooms + /// (regardless of type) are returned. + QStringList roomTypes; }; // Construction/destruction @@ -211,6 +216,7 @@ struct JsonObjectConverter<QueryPublicRoomsJob::Filter> { { addParam<IfNotEmpty>(jo, QStringLiteral("generic_search_term"), pod.genericSearchTerm); + addParam<IfNotEmpty>(jo, QStringLiteral("room_types"), pod.roomTypes); } }; diff --git a/lib/csapi/login.cpp b/lib/csapi/login.cpp index a5bac9ea..7bb74e29 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -4,37 +4,41 @@ #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/v3", "/login")); } GetLoginFlowsJob::GetLoginFlowsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"), - QStringLiteral("/_matrix/client/r0") % "/login", false) + makePath("/_matrix/client/v3", "/login"), false) {} LoginJob::LoginJob(const QString& type, const Omittable<UserIdentifier>& identifier, const QString& password, const QString& token, const QString& deviceId, - const QString& initialDeviceDisplayName) + const QString& initialDeviceDisplayName, + Omittable<bool> refreshToken) : BaseJob(HttpVerb::Post, QStringLiteral("LoginJob"), - QStringLiteral("/_matrix/client/r0") % "/login", false) + makePath("/_matrix/client/v3", "/login"), false) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("type"), type); - addParam<IfNotEmpty>(_data, QStringLiteral("identifier"), identifier); - addParam<IfNotEmpty>(_data, QStringLiteral("password"), password); - addParam<IfNotEmpty>(_data, QStringLiteral("token"), token); - addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("type"), type); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("identifier"), identifier); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("password"), password); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("token"), token); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_id"), deviceId); + addParam<IfNotEmpty>(_dataJson, + QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); - setRequestData(std::move(_data)); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); + addExpectedKey("user_id"); + addExpectedKey("access_token"); + addExpectedKey("device_id"); } diff --git a/lib/csapi/login.h b/lib/csapi/login.h index b35db1eb..b9f14266 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -16,7 +16,7 @@ namespace Quotient { * Gets the homeserver's supported login types to authenticate users. Clients * should pick one of these and supply it as the `type` when logging in. */ -class GetLoginFlowsJob : public BaseJob { +class QUOTIENT_API GetLoginFlowsJob : public BaseJob { public: // Inner data structures @@ -73,7 +73,7 @@ struct JsonObjectConverter<GetLoginFlowsJob::LoginFlow> { * [Relationship between access tokens and * devices](/client-server-api/#relationship-between-access-tokens-and-devices). */ -class LoginJob : public BaseJob { +class QUOTIENT_API LoginJob : public BaseJob { public: /*! \brief Authenticates the user. * @@ -111,12 +111,16 @@ public: * \param initialDeviceDisplayName * A display name to assign to the newly-created device. Ignored * if `device_id` corresponds to a known device. + * + * \param refreshToken + * If true, the client supports refresh tokens. */ explicit LoginJob(const QString& type, const Omittable<UserIdentifier>& identifier = none, const QString& password = {}, const QString& token = {}, const QString& deviceId = {}, - const QString& initialDeviceDisplayName = {}); + const QString& initialDeviceDisplayName = {}, + Omittable<bool> refreshToken = none); // Result properties @@ -130,15 +134,23 @@ public: return loadFromJson<QString>("access_token"_ls); } - /// The server_name of the homeserver on which the account has - /// been registered. - /// - /// **Deprecated**. Clients should extract the server_name from - /// `user_id` (by splitting at the first colon) if they require - /// it. Note also that `homeserver` is not spelt this way. - QString homeServer() const + /// A refresh token for the account. This token can be used to + /// obtain a new access token when it expires by calling the + /// `/refresh` endpoint. + QString refreshToken() const + { + return loadFromJson<QString>("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. Once + /// the access token has expired a new access token can be + /// obtained by using the provided refresh token. If no + /// refresh token is provided, the client will need to re-log in + /// to obtain a new access token. If not given, the client can + /// assume that the access token will not expire. + Omittable<int> expiresInMs() const { - return loadFromJson<QString>("home_server"_ls); + return loadFromJson<Omittable<int>>("expires_in_ms"_ls); } /// ID of the logged-in device. Will be the same as the diff --git a/lib/csapi/logout.cpp b/lib/csapi/logout.cpp index 9583b8ec..9ec54c71 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/v3", "/logout")); } LogoutJob::LogoutJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"), - QStringLiteral("/_matrix/client/r0") % "/logout") + makePath("/_matrix/client/v3", "/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/v3", "/logout/all")); } LogoutAllJob::LogoutAllJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"), - QStringLiteral("/_matrix/client/r0") % "/logout/all") + makePath("/_matrix/client/v3", "/logout/all")) {} diff --git a/lib/csapi/logout.h b/lib/csapi/logout.h index 2e4c2692..3f1ac7fa 100644 --- a/lib/csapi/logout.h +++ b/lib/csapi/logout.h @@ -15,7 +15,7 @@ namespace Quotient { * [Device keys](/client-server-api/#device-keys) for the device are deleted * alongside the device. */ -class LogoutJob : public BaseJob { +class QUOTIENT_API LogoutJob : public BaseJob { public: /// Invalidates a user access token explicit LogoutJob(); @@ -44,7 +44,7 @@ public: * used in the request, and therefore the attacker is unable to take over the * account in this way. */ -class LogoutAllJob : public BaseJob { +class QUOTIENT_API LogoutAllJob : public BaseJob { public: /// Invalidates all access tokens for a user explicit LogoutAllJob(); diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index 855c051f..0b2c99ce 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -4,16 +4,14 @@ #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; - addParam<>(_q, QStringLiteral("from"), from); + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); addParam<>(_q, QStringLiteral("dir"), dir); addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); @@ -22,21 +20,23 @@ auto queryToGetRoomEvents(const QString& from, const QString& to, } QUrl GetRoomEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, - const QString& from, const QString& dir, + const QString& dir, const QString& from, const QString& to, Omittable<int> limit, const QString& filter) { return BaseJob::makeRequestUrl( std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId % "/messages", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"), queryToGetRoomEvents(from, to, dir, limit, filter)); } -GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& from, - const QString& dir, const QString& to, +GetRoomEventsJob::GetRoomEventsJob(const QString& roomId, const QString& dir, + const QString& from, const QString& to, Omittable<int> limit, const QString& filter) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomEventsJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/messages", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/messages"), queryToGetRoomEvents(from, to, dir, limit, filter)) -{} +{ + addExpectedKey("start"); + addExpectedKey("chunk"); +} diff --git a/lib/csapi/message_pagination.h b/lib/csapi/message_pagination.h index 363e4d99..b4f3a38a 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -4,7 +4,7 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" #include "jobs/basejob.h" namespace Quotient { @@ -18,27 +18,37 @@ namespace Quotient { * [Lazy-loading room members](/client-server-api/#lazy-loading-room-members) * for more information. */ -class GetRoomEventsJob : public BaseJob { +class QUOTIENT_API GetRoomEventsJob : public BaseJob { public: /*! \brief Get a list of events for this room * * \param roomId * The room to get events from. * + * \param dir + * The direction to return events from. If this is set to `f`, events + * will be returned in chronological order starting at `from`. If it + * is set to `b`, events will be returned in *reverse* chronological + * order, again starting at `from`. + * * \param from * The token to start returning events from. This token can be obtained - * from a `prev_batch` token returned for each room by the sync API, - * or from a `start` or `end` token returned by a previous request - * to this endpoint. + * from a `prev_batch` or `next_batch` token returned by the `/sync` + * endpoint, or from an `end` token returned by a previous request to this + * endpoint. * - * \param dir - * The direction to return events from. + * This endpoint can also accept a value returned as a `start` token + * by a previous request to this endpoint, though servers are not + * required to support this. Clients should not rely on the behaviour. + * + * If it is not provided, the homeserver shall return a list of messages + * from the first or last (per the value of the `dir` parameter) visible + * event in the room history for the requesting user. * * \param to * The token to stop returning events at. This token can be obtained from - * a `prev_batch` token returned for each room by the sync endpoint, - * or from a `start` or `end` token returned by a previous request to - * this endpoint. + * a `prev_batch` or `next_batch` token returned by the `/sync` endpoint, + * or from an `end` token returned by a previous request to this endpoint. * * \param limit * The maximum number of events to return. Default: 10. @@ -46,8 +56,8 @@ public: * \param filter * A JSON RoomEventFilter to filter returned events with. */ - explicit GetRoomEventsJob(const QString& roomId, const QString& from, - const QString& dir, const QString& to = {}, + explicit GetRoomEventsJob(const QString& roomId, const QString& dir, + const QString& from = {}, const QString& to = {}, Omittable<int> limit = none, const QString& filter = {}); @@ -57,25 +67,34 @@ public: * is necessary but the job itself isn't. */ static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, - const QString& from, const QString& dir, + const QString& dir, const QString& from = {}, const QString& to = {}, Omittable<int> limit = none, const QString& filter = {}); // Result properties - /// The token the pagination starts from. If `dir=b` this will be - /// the token supplied in `from`. + /// A token corresponding to the start of `chunk`. This will be the same as + /// the value given in `from`. QString begin() const { return loadFromJson<QString>("start"_ls); } - /// The token the pagination ends at. If `dir=b` this token should - /// be used again to request even earlier events. + /// A token corresponding to the end of `chunk`. This token can be passed + /// back to this endpoint to request further events. + /// + /// If no further events are available (either because we have + /// reached the start of the timeline, or because the user does + /// not have permission to see any more events), this property + /// is omitted from the response. 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. + /// for `dir=f` in chronological order. (The exact definition of + /// `chronological` is dependent on the server implementation.) + /// + /// Note that an empty `chunk` does not *necessarily* imply that no more + /// events are available. Clients should continue to paginate until no `end` + /// property is returned. RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } /// A list of state events relevant to showing the `chunk`. For example, if @@ -86,7 +105,7 @@ public: /// may remove membership events which would have already been /// sent to the client in prior calls to this endpoint, assuming /// the membership of those members has not changed. - StateEvents state() { return takeFromJson<StateEvents>("state"_ls); } + RoomEvents state() { return takeFromJson<RoomEvents>("state"_ls); } }; } // namespace Quotient diff --git a/lib/csapi/notifications.cpp b/lib/csapi/notifications.cpp index a479d500..38aed174 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/v3", + "/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/v3", "/notifications"), queryToGetNotifications(from, limit, only)) { addExpectedKey("notifications"); diff --git a/lib/csapi/notifications.h b/lib/csapi/notifications.h index 0999fece..ff8aa47f 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -4,7 +4,7 @@ #pragma once -#include "events/eventloader.h" +#include "events/event.h" #include "jobs/basejob.h" namespace Quotient { @@ -14,7 +14,7 @@ namespace Quotient { * This API is used to paginate through the list of events that the * user has been, or would have been notified about. */ -class GetNotificationsJob : public BaseJob { +class QUOTIENT_API GetNotificationsJob : public BaseJob { public: // Inner data structures @@ -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 @@ -43,7 +43,8 @@ public: /*! \brief Gets a list of events that the user has been notified about * * \param from - * Pagination token given to retrieve the next set of events. + * Pagination token to continue from. This should be the `next_token` + * returned from an earlier call to this endpoint. * * \param limit * Limit on the number of events to return in this request. diff --git a/lib/csapi/openid.cpp b/lib/csapi/openid.cpp index 3941e9c0..7e89b8a6 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/v3", "/user/", userId, + "/openid/request_token")) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); } diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 0be39c8c..b3f72a25 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -21,7 +21,7 @@ namespace Quotient { * be used to request another OpenID access token or call `/sync`, for * example. */ -class RequestOpenIdTokenJob : public BaseJob { +class QUOTIENT_API RequestOpenIdTokenJob : public BaseJob { public: /*! \brief Get an OpenID token object to verify the requester's identity. * @@ -43,7 +43,10 @@ public: /// 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()); } + OpenIdCredentials tokenData() const + { + return fromJson<OpenIdCredentials>(jsonData()); + } }; } // namespace Quotient diff --git a/lib/csapi/peeking_events.cpp b/lib/csapi/peeking_events.cpp index 70a5b6f3..9dd1445e 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/v3", "/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/v3", "/events"), queryToPeekEvents(from, timeout, roomId)) {} diff --git a/lib/csapi/peeking_events.h b/lib/csapi/peeking_events.h index 885ff340..a67d2e4a 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -4,12 +4,12 @@ #pragma once -#include "events/eventloader.h" +#include "events/roomevent.h" #include "jobs/basejob.h" namespace Quotient { -/*! \brief Listen on the event stream. +/*! \brief Listen on the event stream of a particular room. * * 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 @@ -22,9 +22,9 @@ namespace Quotient { * API will also be deprecated at some point, but its replacement is not * yet known. */ -class PeekEventsJob : public BaseJob { +class QUOTIENT_API PeekEventsJob : public BaseJob { public: - /*! \brief Listen on the event stream. + /*! \brief Listen on the event stream of a particular room. * * \param from * The token to stream from. This token is either from a previous diff --git a/lib/csapi/presence.cpp b/lib/csapi/presence.cpp index 58d0d157..828ccfb7 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -4,33 +4,29 @@ #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/v3", "/presence/", userId, "/status")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("presence"), presence); - addParam<IfNotEmpty>(_data, QStringLiteral("status_msg"), statusMsg); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("presence"), presence); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("status_msg"), statusMsg); + setRequestData({ _dataJson }); } QUrl GetPresenceJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/presence/" % userId % "/status"); + makePath("/_matrix/client/v3", "/presence/", + userId, "/status")); } GetPresenceJob::GetPresenceJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"), - QStringLiteral("/_matrix/client/r0") % "/presence/" % userId - % "/status") + makePath("/_matrix/client/v3", "/presence/", userId, "/status")) { addExpectedKey("presence"); } diff --git a/lib/csapi/presence.h b/lib/csapi/presence.h index 4ab50e25..52445205 100644 --- a/lib/csapi/presence.h +++ b/lib/csapi/presence.h @@ -15,7 +15,7 @@ namespace Quotient { * not need to specify the `last_active_ago` field. You cannot set the * presence state of another user. */ -class SetPresenceJob : public BaseJob { +class QUOTIENT_API SetPresenceJob : public BaseJob { public: /*! \brief Update this user's presence state. * @@ -36,7 +36,7 @@ public: * * Get the given user's presence state. */ -class GetPresenceJob : public BaseJob { +class QUOTIENT_API GetPresenceJob : public BaseJob { public: /*! \brief Get this user's presence state. * diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 8436b8e6..f024ed82 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -4,67 +4,63 @@ #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/v3", "/profile/", userId, + "/displayname")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("displayname"), displayname); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("displayname"), displayname); + setRequestData({ _dataJson }); } QUrl GetDisplayNameJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/profile/" % userId % "/displayname"); + makePath("/_matrix/client/v3", "/profile/", + userId, "/displayname")); } GetDisplayNameJob::GetDisplayNameJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId - % "/displayname", + makePath("/_matrix/client/v3", "/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/v3", "/profile/", userId, "/avatar_url")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("avatar_url"), avatarUrl); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("avatar_url"), avatarUrl); + setRequestData({ _dataJson }); } 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/v3", "/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/v3", "/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/v3", "/profile/", + userId)); } GetUserProfileJob::GetUserProfileJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"), - QStringLiteral("/_matrix/client/r0") % "/profile/" % userId, false) + makePath("/_matrix/client/v3", "/profile/", userId), false) {} diff --git a/lib/csapi/profile.h b/lib/csapi/profile.h index 8bbe4f8c..b00c944b 100644 --- a/lib/csapi/profile.h +++ b/lib/csapi/profile.h @@ -13,7 +13,7 @@ namespace Quotient { * This API sets the given user's display name. You must have permission to * set this user's display name, e.g. you need to have their `access_token`. */ -class SetDisplayNameJob : public BaseJob { +class QUOTIENT_API SetDisplayNameJob : public BaseJob { public: /*! \brief Set the user's display name. * @@ -33,7 +33,7 @@ public: * own displayname or to query the name of other users; either locally or * on remote homeservers. */ -class GetDisplayNameJob : public BaseJob { +class QUOTIENT_API GetDisplayNameJob : public BaseJob { public: /*! \brief Get the user's display name. * @@ -63,7 +63,7 @@ public: * This API sets the given user's avatar URL. You must have permission to * set this user's avatar URL, e.g. you need to have their `access_token`. */ -class SetAvatarUrlJob : public BaseJob { +class QUOTIENT_API SetAvatarUrlJob : public BaseJob { public: /*! \brief Set the user's avatar URL. * @@ -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. @@ -82,7 +82,7 @@ public: * own avatar URL or to query the URL of other users; either locally or * on remote homeservers. */ -class GetAvatarUrlJob : public BaseJob { +class QUOTIENT_API GetAvatarUrlJob : public BaseJob { public: /*! \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. @@ -111,7 +111,7 @@ public: * locally or on remote homeservers. This API may return keys which are not * limited to `displayname` or `avatar_url`. */ -class GetUserProfileJob : public BaseJob { +class QUOTIENT_API GetUserProfileJob : public BaseJob { public: /*! \brief Get this user's profile information. * @@ -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..fb6595fc 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/v3", "/pushers")); } GetPushersJob::GetPushersJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"), - QStringLiteral("/_matrix/client/r0") % "/pushers") + makePath("/_matrix/client/v3", "/pushers")) {} PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, @@ -26,17 +23,18 @@ 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/v3", "/pushers/set")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("pushkey"), pushkey); - addParam<>(_data, QStringLiteral("kind"), kind); - addParam<>(_data, QStringLiteral("app_id"), appId); - addParam<>(_data, QStringLiteral("app_display_name"), appDisplayName); - addParam<>(_data, QStringLiteral("device_display_name"), deviceDisplayName); - addParam<IfNotEmpty>(_data, QStringLiteral("profile_tag"), profileTag); - addParam<>(_data, QStringLiteral("lang"), lang); - addParam<>(_data, QStringLiteral("data"), data); - addParam<IfNotEmpty>(_data, QStringLiteral("append"), append); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("pushkey"), pushkey); + addParam<>(_dataJson, QStringLiteral("kind"), kind); + addParam<>(_dataJson, QStringLiteral("app_id"), appId); + addParam<>(_dataJson, QStringLiteral("app_display_name"), appDisplayName); + addParam<>(_dataJson, QStringLiteral("device_display_name"), + deviceDisplayName); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("profile_tag"), profileTag); + addParam<>(_dataJson, QStringLiteral("lang"), lang); + addParam<>(_dataJson, QStringLiteral("data"), data); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("append"), append); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/pusher.h b/lib/csapi/pusher.h index 13c9ec25..d859ffc4 100644 --- a/lib/csapi/pusher.h +++ b/lib/csapi/pusher.h @@ -12,7 +12,7 @@ namespace Quotient { * * Gets all currently active pushers for the authenticated user. */ -class GetPushersJob : public BaseJob { +class QUOTIENT_API GetPushersJob : public BaseJob { public: // Inner data structures @@ -21,7 +21,7 @@ public: struct PusherData { /// 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; @@ -108,7 +108,7 @@ struct JsonObjectConverter<GetPushersJob::Pusher> { * [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 { +class QUOTIENT_API PostPusherJob : public BaseJob { public: // Inner data structures @@ -119,7 +119,7 @@ public: /// 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; + QUrl url; /// The format to send notifications in to Push Gateways if the /// `kind` is `http`. The details about what fields the /// homeserver should send to the push gateway are defined in the diff --git a/lib/csapi/pushrules.cpp b/lib/csapi/pushrules.cpp index 86165744..2376654a 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/v3", "/pushrules")); } GetPushRulesJob::GetPushRulesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"), - QStringLiteral("/_matrix/client/r0") % "/pushrules") + makePath("/_matrix/client/v3", "/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/v3", "/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/v3", "/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/v3", "/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/v3", "/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,15 +65,15 @@ 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/v3", "/pushrules/", scope, "/", kind, + "/", ruleId), queryToSetPushRule(before, after)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("actions"), actions); - addParam<IfNotEmpty>(_data, QStringLiteral("conditions"), conditions); - addParam<IfNotEmpty>(_data, QStringLiteral("pattern"), pattern); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("actions"), actions); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("conditions"), conditions); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("pattern"), pattern); + setRequestData({ _dataJson }); } QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, @@ -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/v3", "/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/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { addExpectedKey("enabled"); } @@ -105,12 +100,12 @@ 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/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/enabled")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("enabled"), enabled); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("enabled"), enabled); + setRequestData({ _dataJson }); } QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, @@ -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/v3", "/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/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { addExpectedKey("actions"); } @@ -138,10 +133,10 @@ 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/v3", "/pushrules/", scope, "/", kind, + "/", ruleId, "/actions")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("actions"), actions); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("actions"), actions); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/pushrules.h b/lib/csapi/pushrules.h index a5eb48f0..d6c57efd 100644 --- a/lib/csapi/pushrules.h +++ b/lib/csapi/pushrules.h @@ -19,7 +19,7 @@ namespace Quotient { * `/pushrules/global/`. This will return a subset of this data under the * specified key e.g. the `global` key. */ -class GetPushRulesJob : public BaseJob { +class QUOTIENT_API GetPushRulesJob : public BaseJob { public: /// Retrieve all push rulesets. explicit GetPushRulesJob(); @@ -44,7 +44,7 @@ public: * * Retrieve a single specified push rule. */ -class GetPushRuleJob : public BaseJob { +class QUOTIENT_API GetPushRuleJob : public BaseJob { public: /*! \brief Retrieve a push rule. * @@ -79,7 +79,7 @@ public: * * This endpoint removes the push rule defined in the path. */ -class DeletePushRuleJob : public BaseJob { +class QUOTIENT_API DeletePushRuleJob : public BaseJob { public: /*! \brief Delete a push rule. * @@ -112,7 +112,7 @@ public: * * When creating push rules, they MUST be enabled by default. */ -class SetPushRuleJob : public BaseJob { +class QUOTIENT_API SetPushRuleJob : public BaseJob { public: /*! \brief Add or change a push rule. * @@ -160,7 +160,7 @@ public: * * This endpoint gets whether the specified push rule is enabled. */ -class IsPushRuleEnabledJob : public BaseJob { +class QUOTIENT_API IsPushRuleEnabledJob : public BaseJob { public: /*! \brief Get whether a push rule is enabled * @@ -195,7 +195,7 @@ public: * * This endpoint allows clients to enable or disable the specified push rule. */ -class SetPushRuleEnabledJob : public BaseJob { +class QUOTIENT_API SetPushRuleEnabledJob : public BaseJob { public: /*! \brief Enable or disable a push rule. * @@ -219,7 +219,7 @@ public: * * This endpoint get the actions for the specified push rule. */ -class GetPushRuleActionsJob : public BaseJob { +class QUOTIENT_API GetPushRuleActionsJob : public BaseJob { public: /*! \brief The actions for a push rule * @@ -258,7 +258,7 @@ public: * This endpoint allows clients to change the actions of a push rule. * This can be used to change the actions of builtin rules. */ -class SetPushRuleActionsJob : public BaseJob { +class QUOTIENT_API SetPushRuleActionsJob : public BaseJob { public: /*! \brief Set the actions for a push rule. * diff --git a/lib/csapi/read_markers.cpp b/lib/csapi/read_markers.cpp index 39e4d148..febd6d3a 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -4,19 +4,19 @@ #include "read_markers.h" -#include <QtCore/QStringBuilder> - using namespace Quotient; SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, - const QString& mRead) + const QString& mRead, + const QString& mReadPrivate) : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/read_markers") + makePath("/_matrix/client/v3", "/rooms/", roomId, "/read_markers")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("m.fully_read"), mFullyRead); - addParam<IfNotEmpty>(_data, QStringLiteral("m.read"), mRead); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.fully_read"), mFullyRead); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read"), mRead); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read.private"), + mReadPrivate); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/read_markers.h b/lib/csapi/read_markers.h index 00a2aa0d..1024076f 100644 --- a/lib/csapi/read_markers.h +++ b/lib/csapi/read_markers.h @@ -13,7 +13,7 @@ namespace Quotient { * Sets the position of the read marker for a given room, and optionally * the read receipt's location. */ -class SetReadMarkerJob : public BaseJob { +class QUOTIENT_API SetReadMarkerJob : public BaseJob { public: /*! \brief Set the position of the read marker for a room. * @@ -28,9 +28,16 @@ public: * The event ID to set the read receipt location at. This is * equivalent to calling `/receipt/m.read/$elsewhere:example.org` * and is provided here to save that extra call. + * + * \param mReadPrivate + * The event ID to set the *private* read receipt location at. This + * equivalent to calling `/receipt/m.read.private/$elsewhere:example.org` + * and is provided here to save that extra call. */ - explicit SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, - const QString& mRead = {}); + explicit SetReadMarkerJob(const QString& roomId, + const QString& mFullyRead = {}, + const QString& mRead = {}, + const QString& mReadPrivate = {}); }; } // namespace Quotient diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index 00d1c28a..0194603d 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/v3", "/rooms/", roomId, "/receipt/", + receiptType, "/", eventId)) { - setRequestData(Data(toJson(receipt))); + setRequestData({ toJson(receipt) }); } diff --git a/lib/csapi/receipts.h b/lib/csapi/receipts.h index 7ac093cd..98bc5004 100644 --- a/lib/csapi/receipts.h +++ b/lib/csapi/receipts.h @@ -13,7 +13,7 @@ namespace Quotient { * This API updates the marker for the given receipt type to the event ID * specified. */ -class PostReceiptJob : public BaseJob { +class QUOTIENT_API PostReceiptJob : public BaseJob { public: /*! \brief Send a receipt for the given event ID. * @@ -21,7 +21,13 @@ public: * The room in which to send the event. * * \param receiptType - * The type of receipt to send. + * The type of receipt to send. This can also be `m.fully_read` as an + * alternative to + * [`/read_makers`](/client-server-api/#post_matrixclientv3roomsroomidread_markers). + * + * Note that `m.fully_read` does not appear under `m.receipt`: this + * endpoint effectively calls `/read_markers` internally when presented with + * a receipt type of `m.fully_read`. * * \param eventId * The event ID to acknowledge up to. diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index 91497064..154abd9b 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -4,17 +4,15 @@ #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/v3", "/rooms/", roomId, "/redact/", + eventId, "/", txnId)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/redaction.h b/lib/csapi/redaction.h index f0db9f9f..2f85793e 100644 --- a/lib/csapi/redaction.h +++ b/lib/csapi/redaction.h @@ -22,7 +22,7 @@ namespace Quotient { * * Server administrators may redact events sent by users on their server. */ -class RedactEventJob : public BaseJob { +class QUOTIENT_API RedactEventJob : public BaseJob { public: /*! \brief Strips all non-integrity-critical information out of an event. * @@ -33,9 +33,9 @@ public: * The ID of the event to redact * * \param txnId - * The transaction ID for this event. Clients should generate a - * unique ID; it will be used by the server to ensure idempotency of - * requests. + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate a unique ID; it will be used by the + * server to ensure idempotency of requests. * * \param reason * The reason for the event being redacted. diff --git a/lib/csapi/refresh.cpp b/lib/csapi/refresh.cpp new file mode 100644 index 00000000..284ae4ff --- /dev/null +++ b/lib/csapi/refresh.cpp @@ -0,0 +1,18 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "refresh.h" + +using namespace Quotient; + +RefreshJob::RefreshJob(const QString& refreshToken) + : BaseJob(HttpVerb::Post, QStringLiteral("RefreshJob"), + makePath("/_matrix/client/v3", "/refresh"), false) +{ + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); + addExpectedKey("access_token"); +} diff --git a/lib/csapi/refresh.h b/lib/csapi/refresh.h new file mode 100644 index 00000000..d432802c --- /dev/null +++ b/lib/csapi/refresh.h @@ -0,0 +1,65 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Refresh an access token + * + * Refresh an access token. Clients should use the returned access token + * when making subsequent API calls, and store the returned refresh token + * (if given) in order to refresh the new access token when necessary. + * + * After an access token has been refreshed, a server can choose to + * invalidate the old access token immediately, or can choose not to, for + * example if the access token would expire soon anyways. Clients should + * not make any assumptions about the old access token still being valid, + * and should use the newly provided access token instead. + * + * The old refresh token remains valid until the new access token or refresh + * token is used, at which point the old refresh token is revoked. + * + * Note that this endpoint does not require authentication via an + * access token. Authentication is provided via the refresh token. + * + * Application Service identity assertion is disabled for this endpoint. + */ +class QUOTIENT_API RefreshJob : public BaseJob { +public: + /*! \brief Refresh an access token + * + * \param refreshToken + * The refresh token + */ + explicit RefreshJob(const QString& refreshToken = {}); + + // Result properties + + /// The new access token to use. + QString accessToken() const + { + return loadFromJson<QString>("access_token"_ls); + } + + /// The new refresh token to use when the access token needs to + /// be refreshed again. If not given, the old refresh token can + /// be re-used. + QString refreshToken() const + { + return loadFromJson<QString>("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. If not + /// given, the client can assume that the access token will not + /// expire. + Omittable<int> expiresInMs() const + { + return loadFromJson<Omittable<int>>("expires_in_ms"_ls); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/registration.cpp b/lib/csapi/registration.cpp index b80abc84..04c0fe12 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; } @@ -20,93 +18,97 @@ RegisterJob::RegisterJob(const QString& kind, const QString& username, const QString& password, const QString& deviceId, const QString& initialDeviceDisplayName, - Omittable<bool> inhibitLogin) + Omittable<bool> inhibitLogin, + Omittable<bool> refreshToken) : BaseJob(HttpVerb::Post, QStringLiteral("RegisterJob"), - QStringLiteral("/_matrix/client/r0") % "/register", + makePath("/_matrix/client/v3", "/register"), queryToRegister(kind), {}, false) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - addParam<IfNotEmpty>(_data, QStringLiteral("username"), username); - addParam<IfNotEmpty>(_data, QStringLiteral("password"), password); - addParam<IfNotEmpty>(_data, QStringLiteral("device_id"), deviceId); - addParam<IfNotEmpty>(_data, QStringLiteral("initial_device_display_name"), + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("username"), username); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("password"), password); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("device_id"), deviceId); + addParam<IfNotEmpty>(_dataJson, + QStringLiteral("initial_device_display_name"), initialDeviceDisplayName); - addParam<IfNotEmpty>(_data, QStringLiteral("inhibit_login"), inhibitLogin); - setRequestData(std::move(_data)); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("inhibit_login"), + inhibitLogin); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("refresh_token"), + refreshToken); + setRequestData({ _dataJson }); addExpectedKey("user_id"); } RequestTokenToRegisterEmailJob::RequestTokenToRegisterEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/register/email/requestToken", + makePath("/_matrix/client/v3", "/register/email/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/register/msisdn/requestToken", + makePath("/_matrix/client/v3", "/register/msisdn/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); } ChangePasswordJob::ChangePasswordJob(const QString& newPassword, - Omittable<bool> logoutDevices, + bool logoutDevices, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), - QStringLiteral("/_matrix/client/r0") % "/account/password") + makePath("/_matrix/client/v3", "/account/password")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("new_password"), newPassword); - addParam<IfNotEmpty>(_data, QStringLiteral("logout_devices"), logoutDevices); - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("new_password"), newPassword); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("logout_devices"), + logoutDevices); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + setRequestData({ _dataJson }); } RequestTokenToResetPasswordEmailJob::RequestTokenToResetPasswordEmailJob( const EmailValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordEmailJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/password/email/requestToken", + makePath("/_matrix/client/v3", + "/account/password/email/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordMSISDNJob"), - QStringLiteral("/_matrix/client/r0") - % "/account/password/msisdn/requestToken", + makePath("/_matrix/client/v3", + "/account/password/msisdn/requestToken"), false) { - setRequestData(Data(toJson(body))); + setRequestData({ 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/v3", "/account/deactivate")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("auth"), auth); - addParam<IfNotEmpty>(_data, QStringLiteral("id_server"), idServer); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("auth"), auth); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("id_server"), idServer); + setRequestData({ _dataJson }); addExpectedKey("id_server_unbind_result"); } auto queryToCheckUsernameAvailability(const QString& username) { - BaseJob::Query _q; + QUrlQuery _q; addParam<>(_q, QStringLiteral("username"), username); return _q; } @@ -115,13 +117,14 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), - QStringLiteral("/_matrix/client/r0") - % "/register/available", + makePath("/_matrix/client/v3", + "/register/available"), queryToCheckUsernameAvailability(username)); } -CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) +CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob( + const QString& username) : BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"), - QStringLiteral("/_matrix/client/r0") % "/register/available", + makePath("/_matrix/client/v3", "/register/available"), queryToCheckUsernameAvailability(username), {}, false) {} diff --git a/lib/csapi/registration.h b/lib/csapi/registration.h index ae8fc162..21d7f9d7 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -59,7 +59,7 @@ namespace Quotient { * Any user ID returned by this API must conform to the grammar given in the * [Matrix specification](/appendices/#user-identifiers). */ -class RegisterJob : public BaseJob { +class QUOTIENT_API RegisterJob : public BaseJob { public: /*! \brief Register for an account on this homeserver. * @@ -93,6 +93,9 @@ public: * If true, an `access_token` and `device_id` should not be * returned from this call, therefore preventing an automatic * login. Defaults to false. + * + * \param refreshToken + * If true, the client supports refresh tokens. */ explicit RegisterJob(const QString& kind = QStringLiteral("user"), const Omittable<AuthenticationData>& auth = none, @@ -100,7 +103,8 @@ public: const QString& password = {}, const QString& deviceId = {}, const QString& initialDeviceDisplayName = {}, - Omittable<bool> inhibitLogin = none); + Omittable<bool> inhibitLogin = none, + Omittable<bool> refreshToken = none); // Result properties @@ -118,15 +122,27 @@ public: return loadFromJson<QString>("access_token"_ls); } - /// The server_name of the homeserver on which the account has - /// been registered. + /// A refresh token for the account. This token can be used to + /// obtain a new access token when it expires by calling the + /// `/refresh` endpoint. + /// + /// Omitted if the `inhibit_login` option is true. + QString refreshToken() const + { + return loadFromJson<QString>("refresh_token"_ls); + } + + /// The lifetime of the access token, in milliseconds. Once + /// the access token has expired a new access token can be + /// obtained by using the provided refresh token. If no + /// refresh token is provided, the client will need to re-log in + /// to obtain a new access token. If not given, the client can + /// assume that the access token will not expire. /// - /// **Deprecated**. Clients should extract the server_name from - /// `user_id` (by splitting at the first colon) if they require - /// it. Note also that `homeserver` is not spelt this way. - QString homeServer() const + /// Omitted if the `inhibit_login` option is true. + Omittable<int> expiresInMs() const { - return loadFromJson<QString>("home_server"_ls); + return loadFromJson<Omittable<int>>("expires_in_ms"_ls); } /// ID of the registered device. Will be the same as the @@ -143,7 +159,7 @@ public: * should validate the email itself, either by sending a validation email * itself or by using a service it has control over. */ -class RequestTokenToRegisterEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenToRegisterEmailJob : public BaseJob { public: /*! \brief Begins the validation process for an email to be used during * registration. @@ -175,7 +191,7 @@ public: * should validate the phone number itself, either by sending a validation * message itself or by using a service it has control over. */ -class RequestTokenToRegisterMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenToRegisterMSISDNJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given phone number for * the purpose of registering an account @@ -215,7 +231,7 @@ public: * access token provided in the request. Whether other access tokens for * the user are revoked depends on the request parameters. */ -class ChangePasswordJob : public BaseJob { +class QUOTIENT_API ChangePasswordJob : public BaseJob { public: /*! \brief Changes a user's password. * @@ -227,14 +243,15 @@ public: * should be revoked if the request succeeds. * * When `false`, the server can still take advantage of the [soft logout - * method](/client-server-api/#soft-logout) for the user's remaining devices. + * method](/client-server-api/#soft-logout) for the user's remaining + * devices. * * \param auth * Additional authentication information for the user-interactive * authentication API. */ explicit ChangePasswordJob(const QString& newPassword, - Omittable<bool> logoutDevices = none, + bool logoutDevices = true, const Omittable<AuthenticationData>& auth = none); }; @@ -247,7 +264,7 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * 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 @@ -257,7 +274,7 @@ public: * The homeserver should validate the email itself, either by sending a * validation email itself or by using a service it has control over. */ -class RequestTokenToResetPasswordEmailJob : public BaseJob { +class QUOTIENT_API RequestTokenToResetPasswordEmailJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given email address * for the purpose of resetting a user's password @@ -269,7 +286,7 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/email/requestToken`](/client-server-api/#post_matrixclientr0registeremailrequesttoken) + * [`/register/email/requestToken`](/client-server-api/#post_matrixclientv3registeremailrequesttoken) * 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 @@ -299,7 +316,7 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * 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 @@ -309,7 +326,7 @@ public: * The homeserver should validate the phone number itself, either by sending a * validation message itself or by using a service it has control over. */ -class RequestTokenToResetPasswordMSISDNJob : public BaseJob { +class QUOTIENT_API RequestTokenToResetPasswordMSISDNJob : public BaseJob { public: /*! \brief Requests a validation token be sent to the given phone number for * the purpose of resetting a user's password. @@ -321,15 +338,16 @@ public: * `/account/password` endpoint. * * This API's parameters and response are identical to that of the - * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientr0registermsisdnrequesttoken) + * [`/register/msisdn/requestToken`](/client-server-api/#post_matrixclientv3registermsisdnrequesttoken) * endpoint, except that * `M_THREEPID_NOT_FOUND` may be returned if no account matching the * given phone number could be found. The server may instead send the SMS * to the given phone number prompting the user to create an account. * `M_THREEPID_IN_USE` may not be returned. * - * The homeserver should validate the phone number itself, either by sending - * a validation message itself or by using a service it has control over. + * 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 RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body); @@ -361,7 +379,7 @@ public: * parameter because the homeserver is expected to sign the request to the * identity server instead. */ -class DeactivateAccountJob : public BaseJob { +class QUOTIENT_API DeactivateAccountJob : public BaseJob { public: /*! \brief Deactivate a user's account. * @@ -377,8 +395,9 @@ public: * it must return an `id_server_unbind_result` of * `no-support`. */ - explicit DeactivateAccountJob(const Omittable<AuthenticationData>& auth = none, - const QString& idServer = {}); + explicit DeactivateAccountJob( + const Omittable<AuthenticationData>& auth = none, + const QString& idServer = {}); // Result properties @@ -411,7 +430,7 @@ public: * reserve the username. This can mean that the username becomes unavailable * between checking its availability and attempting to register it. */ -class CheckUsernameAvailabilityJob : public BaseJob { +class QUOTIENT_API CheckUsernameAvailabilityJob : public BaseJob { public: /*! \brief Checks to see if a username is available on the server. * diff --git a/lib/csapi/registration_tokens.cpp b/lib/csapi/registration_tokens.cpp new file mode 100644 index 00000000..9c1f0587 --- /dev/null +++ b/lib/csapi/registration_tokens.cpp @@ -0,0 +1,33 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "registration_tokens.h" + +using namespace Quotient; + +auto queryToRegistrationTokenValidity(const QString& token) +{ + QUrlQuery _q; + addParam<>(_q, QStringLiteral("token"), token); + return _q; +} + +QUrl RegistrationTokenValidityJob::makeRequestUrl(QUrl baseUrl, + const QString& token) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", + "/register/m.login.registration_token/validity"), + queryToRegistrationTokenValidity(token)); +} + +RegistrationTokenValidityJob::RegistrationTokenValidityJob(const QString& token) + : BaseJob(HttpVerb::Get, QStringLiteral("RegistrationTokenValidityJob"), + makePath("/_matrix/client/v1", + "/register/m.login.registration_token/validity"), + queryToRegistrationTokenValidity(token), {}, false) +{ + addExpectedKey("valid"); +} diff --git a/lib/csapi/registration_tokens.h b/lib/csapi/registration_tokens.h new file mode 100644 index 00000000..e3008dd4 --- /dev/null +++ b/lib/csapi/registration_tokens.h @@ -0,0 +1,44 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Query if a given registration token is still valid. + * + * Queries the server to determine if a given registration token is still + * valid at the time of request. This is a point-in-time check where the + * token might still expire by the time it is used. + * + * Servers should be sure to rate limit this endpoint to avoid brute force + * attacks. + */ +class QUOTIENT_API RegistrationTokenValidityJob : public BaseJob { +public: + /*! \brief Query if a given registration token is still valid. + * + * \param token + * The token to check validity of. + */ + explicit RegistrationTokenValidityJob(const QString& token); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for RegistrationTokenValidityJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& token); + + // Result properties + + /// True if the token is still valid, false otherwise. This should + /// additionally be false if the token is not a recognised token by + /// the server. + bool valid() const { return loadFromJson<bool>("valid"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/relations.cpp b/lib/csapi/relations.cpp new file mode 100644 index 00000000..1d8febcc --- /dev/null +++ b/lib/csapi/relations.cpp @@ -0,0 +1,118 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "relations.h" + +using namespace Quotient; + +auto queryToGetRelatingEvents(const QString& from, const QString& to, + Omittable<int> limit, const QString& dir) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir); + return _q; +} + +QUrl GetRelatingEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + const QString& from, const QString& to, + Omittable<int> limit, + const QString& dir) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", + roomId, "/relations/", eventId), + queryToGetRelatingEvents(from, to, limit, + dir)); +} + +GetRelatingEventsJob::GetRelatingEventsJob( + const QString& roomId, const QString& eventId, const QString& from, + const QString& to, Omittable<int> limit, const QString& dir) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId), + queryToGetRelatingEvents(from, to, limit, dir)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelType(const QString& from, const QString& to, + Omittable<int> limit, + const QString& dir) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir); + return _q; +} + +QUrl GetRelatingEventsWithRelTypeJob::makeRequestUrl( + QUrl baseUrl, const QString& roomId, const QString& eventId, + const QString& relType, const QString& from, const QString& to, + Omittable<int> limit, const QString& dir) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit, dir)); +} + +GetRelatingEventsWithRelTypeJob::GetRelatingEventsWithRelTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& from, const QString& to, Omittable<int> limit, + const QString& dir) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit, dir)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelTypeAndEventType(const QString& from, + const QString& to, + Omittable<int> limit, + const QString& dir) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("dir"), dir); + return _q; +} + +QUrl GetRelatingEventsWithRelTypeAndEventTypeJob::makeRequestUrl( + QUrl baseUrl, const QString& roomId, const QString& eventId, + const QString& relType, const QString& eventType, const QString& from, + const QString& to, Omittable<int> limit, const QString& dir) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit, dir)); +} + +GetRelatingEventsWithRelTypeAndEventTypeJob:: + GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from, const QString& to, + Omittable<int> limit, const QString& dir) + : BaseJob(HttpVerb::Get, + QStringLiteral("GetRelatingEventsWithRelTypeAndEventTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit, + dir)) +{ + addExpectedKey("chunk"); +} diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h new file mode 100644 index 00000000..5d6efd1c --- /dev/null +++ b/lib/csapi/relations.h @@ -0,0 +1,298 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/roomevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Get the child events for a given parent event. + * + * Retrieve all of the child events for a given parent event. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsJob : public BaseJob { +public: + /*! \brief Get the child events for a given parent event. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. + */ + explicit GetRelatingEventsJob(const QString& roomId, const QString& eventId, + const QString& from = {}, + const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRelatingEventsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& from = {}, + const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } +}; + +/*! \brief Get the child events for a given parent event, with a given + * `relType`. + * + * Retrieve all of the child events for a given parent event which relate to the + * parent using the given `relType`. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsWithRelTypeJob : public BaseJob { +public: + /*! \brief Get the child events for a given parent event, with a given + * `relType`. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param relType + * The [relationship type](/client-server-api/#relationship-types) to + * search for. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. + */ + explicit GetRelatingEventsWithRelTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& from = {}, const QString& to = {}, + Omittable<int> limit = none, const QString& dir = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetRelatingEventsWithRelTypeJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& relType, + const QString& from = {}, const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. The events returned will match the `relType` + /// supplied in the URL. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } +}; + +/*! \brief Get the child events for a given parent event, with a given `relType` + * and `eventType`. + * + * Retrieve all of the child events for a given parent event which relate to the + * parent using the given `relType` and have the given `eventType`. + * + * Note that when paginating the `from` token should be "after" the `to` token + * in terms of topological ordering, because it is only possible to paginate + * "backwards" through events, starting at `from`. + * + * For example, passing a `from` token from page 2 of the results, and a `to` + * token from page 1, would return the empty set. The caller can use a `from` + * token from page 1 and a `to` token from page 2 to paginate over the same + * range, however. + */ +class QUOTIENT_API GetRelatingEventsWithRelTypeAndEventTypeJob + : public BaseJob { +public: + /*! \brief Get the child events for a given parent event, with a given + * `relType` and `eventType`. + * + * \param roomId + * The ID of the room containing the parent event. + * + * \param eventId + * The ID of the parent event whose child events are to be returned. + * + * \param relType + * The [relationship type](/client-server-api/#relationship-types) to + * search for. + * + * \param eventType + * The event type of child events to search for. + * + * Note that in encrypted rooms this will typically always be + * `m.room.encrypted` regardless of the event type contained within the + * encrypted payload. + * + * \param from + * The pagination token to start returning results from. If not supplied, + * results start at the most recent topological event known to the server. + * + * Can be a `next_batch` or `prev_batch` token from a previous call, or a + * returned `start` token from + * [`/messages`](/client-server-api/#get_matrixclientv3roomsroomidmessages), + * or a `next_batch` token from + * [`/sync`](/client-server-api/#get_matrixclientv3sync). + * + * \param to + * The pagination token to stop returning results at. If not supplied, + * results continue up to `limit` or until there are no more events. + * + * Like `from`, this can be a previous token from a prior call to this + * endpoint or from `/messages` or `/sync`. + * + * \param limit + * The maximum number of results to return in a single `chunk`. The server + * can and should apply a maximum value to this parameter to avoid large + * responses. + * + * Similarly, the server should apply a default value when not supplied. + * + * \param dir + * Optional (default `b`) direction to return events from. If this is set + * to `f`, events will be returned in chronological order starting at + * `from`. If it is set to `b`, events will be returned in *reverse* + * chronological order, again starting at `from`. + */ + explicit GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from = {}, + const QString& to = {}, Omittable<int> limit = none, + const QString& dir = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for + * GetRelatingEventsWithRelTypeAndEventTypeJob is necessary but the job + * itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, const QString& relType, + const QString& eventType, + const QString& from = {}, const QString& to = {}, + Omittable<int> limit = none, + const QString& dir = {}); + + // Result properties + + /// The child events of the requested event, ordered topologically + /// most-recent first. The events returned will match the `relType` and + /// `eventType` supplied in the URL. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means there are no more results to fetch and the client should + /// stop paginating. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } + + /// An opaque string representing a pagination token. The absence of this + /// token means this is the start of the result set, i.e. this is the first + /// batch/page. + QString prevBatch() const { return loadFromJson<QString>("prev_batch"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/report_content.cpp b/lib/csapi/report_content.cpp index 0a41625f..bc52208f 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/v3", "/rooms/", roomId, "/report/", + eventId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("score"), score); - addParam<>(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("score"), score); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/report_content.h b/lib/csapi/report_content.h index 375e1829..8c533c19 100644 --- a/lib/csapi/report_content.h +++ b/lib/csapi/report_content.h @@ -13,7 +13,7 @@ namespace Quotient { * Reports an event as inappropriate to the server, which may then notify * the appropriate people. */ -class ReportContentJob : public BaseJob { +class QUOTIENT_API ReportContentJob : public BaseJob { public: /*! \brief Reports an event as inappropriate. * @@ -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..2319496f 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/v3", "/rooms/", roomId, "/send/", + eventType, "/", txnId)) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index 96f5beca..abe5f207 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -16,9 +16,10 @@ 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](/client-server-api/#room-events) for the m. event specification. + * [Room Events](/client-server-api/#room-events) for the m. event + * specification. */ -class SendMessageJob : public BaseJob { +class QUOTIENT_API SendMessageJob : public BaseJob { public: /*! \brief Send a message event to the given room. * @@ -29,9 +30,10 @@ public: * The type of event to send. * * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate an ID unique across requests with the + * same access token; it will be used by the server to ensure idempotency of + * requests. * * \param body * This endpoint is used to send a message event to a room. Message events diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index e18108ac..b4adb739 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/v3", "/rooms/", roomId, "/state/", + eventType, "/", stateKey)) { - setRequestData(Data(toJson(body))); + setRequestData({ toJson(body) }); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_state.h b/lib/csapi/room_state.h index f95af223..a00b0947 100644 --- a/lib/csapi/room_state.h +++ b/lib/csapi/room_state.h @@ -29,7 +29,7 @@ namespace Quotient { * state event is to be sent. Servers do not validate aliases which are * being removed or are already present in the state event. */ -class SetRoomStateWithKeyJob : public BaseJob { +class QUOTIENT_API SetRoomStateWithKeyJob : public BaseJob { public: /*! \brief Send a state event to the given room. * diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp index e3791b08..b03fb6e8 100644 --- a/lib/csapi/room_upgrades.cpp +++ b/lib/csapi/room_upgrades.cpp @@ -4,17 +4,14 @@ #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/v3", "/rooms/", roomId, "/upgrade")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("new_version"), newVersion); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("new_version"), newVersion); + setRequestData({ _dataJson }); addExpectedKey("replacement_room"); } diff --git a/lib/csapi/room_upgrades.h b/lib/csapi/room_upgrades.h index 58327587..0432f667 100644 --- a/lib/csapi/room_upgrades.h +++ b/lib/csapi/room_upgrades.h @@ -12,7 +12,7 @@ namespace Quotient { * * Upgrades the given room to a particular room version. */ -class UpgradeRoomJob : public BaseJob { +class QUOTIENT_API UpgradeRoomJob : public BaseJob { public: /*! \brief Upgrades a room to a new room version. * diff --git a/lib/csapi/rooms.cpp b/lib/csapi/rooms.cpp index 724d941f..563f4fa5 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/v3", "/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/v3", "/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/v3", "/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/v3", "/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/v3", "/rooms/", + roomId, "/state")); } GetRoomStateJob::GetRoomStateJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"), - QStringLiteral("/_matrix/client/r0") % "/rooms/" % roomId - % "/state") + makePath("/_matrix/client/v3", "/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/v3", "/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/v3", "/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/v3", "/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/v3", "/rooms/", roomId, + "/joined_members")) {} diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index 51af2c65..7823a1b0 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -4,8 +4,8 @@ #pragma once -#include "events/eventloader.h" -#include "events/roommemberevent.h" +#include "events/roomevent.h" +#include "events/stateevent.h" #include "jobs/basejob.h" namespace Quotient { @@ -15,7 +15,7 @@ namespace Quotient { * 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 { +class QUOTIENT_API GetOneRoomEventJob : public BaseJob { public: /*! \brief Get a single event by event ID. * @@ -38,7 +38,7 @@ public: // Result properties /// The full event. - EventPtr event() { return fromJson<EventPtr>(jsonData()); } + RoomEventPtr event() { return fromJson<RoomEventPtr>(jsonData()); } }; /*! \brief Get the state identified by the type and key. @@ -48,7 +48,7 @@ public: * state of the room. If the user has left the room then the state is * taken from the state of the room when they left. */ -class GetRoomStateWithKeyJob : public BaseJob { +class QUOTIENT_API GetRoomStateWithKeyJob : public BaseJob { public: /*! \brief Get the state identified by the type and key. * @@ -80,7 +80,7 @@ public: * * Get the state events for the current state of a room. */ -class GetRoomStateJob : public BaseJob { +class QUOTIENT_API GetRoomStateJob : public BaseJob { public: /*! \brief Get all state events in the current state of a room. * @@ -106,7 +106,7 @@ public: * * Get the list of members for this room. */ -class GetMembersByRoomJob : public BaseJob { +class QUOTIENT_API GetMembersByRoomJob : public BaseJob { public: /*! \brief Get the m.room.member events for the room. * @@ -146,10 +146,7 @@ public: // Result properties /// Get the list of members for this room. - EventsArray<RoomMemberEvent> chunk() - { - return takeFromJson<EventsArray<RoomMemberEvent>>("chunk"_ls); - } + StateEvents chunk() { return takeFromJson<StateEvents>("chunk"_ls); } }; /*! \brief Gets the list of currently joined users and their profile data. @@ -157,11 +154,10 @@ public: * This API returns a map of MXIDs to member info objects for members of the * room. The current user must be in the room for it to work, unless it is an * Application Service in which case any of the AS's users must be in the room. - * This API is primarily for Application Services and should be faster to - * respond than `/members` as it can be implemented more efficiently on the - * server. + * This API is primarily for Application Services and should be faster to respond + * than `/members` as it can be implemented more efficiently on the server. */ -class GetJoinedMembersByRoomJob : public BaseJob { +class QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob { public: // Inner data structures @@ -175,7 +171,7 @@ public: /// 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 new file mode 100644 index 00000000..4e2c9e92 --- /dev/null +++ b/lib/csapi/search.cpp @@ -0,0 +1,26 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "search.h" + +using namespace Quotient; + +auto queryToSearch(const QString& nextBatch) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("next_batch"), nextBatch); + return _q; +} + +SearchJob::SearchJob(const Categories& searchCategories, + const QString& nextBatch) + : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"), + makePath("/_matrix/client/v3", "/search"), + queryToSearch(nextBatch)) +{ + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("search_categories"), searchCategories); + setRequestData({ _dataJson }); + addExpectedKey("search_categories"); +} diff --git a/lib/csapi/search.h b/lib/csapi/search.h new file mode 100644 index 00000000..30095f32 --- /dev/null +++ b/lib/csapi/search.h @@ -0,0 +1,307 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "csapi/definitions/room_event_filter.h" + +#include "events/roomevent.h" +#include "events/stateevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Perform a server-side search. + * + * Performs a full text search across different categories. + */ +class QUOTIENT_API SearchJob : public BaseJob { +public: + // Inner data structures + + /// Configures whether any context for the events + /// returned are included in the response. + struct IncludeEventContext { + /// How many events before the result are + /// returned. By default, this is `5`. + Omittable<int> beforeLimit; + /// How many events after the result are + /// returned. By default, this is `5`. + Omittable<int> afterLimit; + /// Requests that the server returns the + /// historic profile information for the users + /// that sent the events that were returned. + /// By default, this is `false`. + Omittable<bool> includeProfile; + }; + + /// Configuration for group. + struct Group { + /// Key that defines the group. + QString key; + }; + + /// Requests that the server partitions the result set + /// based on the provided list of keys. + struct Groupings { + /// List of groups to request. + QVector<Group> groupBy; + }; + + /// Mapping of category name to search criteria. + struct RoomEventsCriteria { + /// The string to search events for + QString searchTerm; + /// The keys to search. Defaults to all. + QStringList keys; + /// This takes a [filter](/client-server-api/#filtering). + RoomEventFilter filter; + /// The order in which to search for results. + /// By default, this is `"rank"`. + QString orderBy; + /// Configures whether any context for the events + /// returned are included in the response. + Omittable<IncludeEventContext> eventContext; + /// Requests the server return the current state for + /// each room returned. + Omittable<bool> includeState; + /// Requests that the server partitions the result set + /// based on the provided list of keys. + Omittable<Groupings> groupings; + }; + + /// Describes which categories to search in and their criteria. + struct Categories { + /// Mapping of category name to search criteria. + Omittable<RoomEventsCriteria> roomEvents; + }; + + /// Performs a full text search across different categories. + struct UserProfile { + /// Performs a full text search across different categories. + QString displayname; + /// Performs a full text search across different categories. + QUrl avatarUrl; + }; + + /// Context for result, if requested. + struct EventContext { + /// Pagination token for the start of the chunk + QString begin; + /// Pagination token for the end of the chunk + QString end; + /// The historic profile information of the + /// users that sent the events returned. + /// + /// The `string` key is the user ID for which + /// the profile belongs to. + QHash<QString, UserProfile> profileInfo; + /// Events just before the result. + RoomEvents eventsBefore; + /// Events just after the result. + RoomEvents eventsAfter; + }; + + /// The result object. + struct Result { + /// A number that describes how closely this result matches the search. + /// Higher is closer. + Omittable<double> rank; + /// The event that matched. + RoomEventPtr result; + /// Context for result, if requested. + Omittable<EventContext> context; + }; + + /// The results for a particular group value. + struct GroupValue { + /// Token that can be used to get the next batch + /// of results in the group, by passing as the + /// `next_batch` parameter to the next call. If + /// this field is absent, there are no more + /// results in this group. + QString nextBatch; + /// Key that can be used to order different + /// groups. + Omittable<int> order; + /// Which results are in this group. + QStringList results; + }; + + /// Mapping of category name to search criteria. + struct ResultRoomEvents { + /// An approximate count of the total number of results found. + Omittable<int> count; + /// List of words which should be highlighted, useful for stemming which + /// may change the query terms. + QStringList highlights; + /// List of results in the requested order. + std::vector<Result> results; + /// The current state for every room in the results. + /// This is included if the request had the + /// `include_state` key set with a value of `true`. + /// + /// The `string` key is the room ID for which the `State + /// Event` array belongs to. + 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: + /// a room's ID or a user's ID). + QHash<QString, QHash<QString, GroupValue>> groups; + /// Token that can be used to get the next batch of + /// results, by passing as the `next_batch` parameter to + /// the next call. If this field is absent, there are no + /// more results. + QString nextBatch; + }; + + /// Describes which categories to search in and their criteria. + struct ResultCategories { + /// Mapping of category name to search criteria. + Omittable<ResultRoomEvents> roomEvents; + }; + + // Construction/destruction + + /*! \brief Perform a server-side search. + * + * \param searchCategories + * Describes which categories to search in and their criteria. + * + * \param nextBatch + * The point to return events from. If given, this should be a + * `next_batch` result from a previous call to this endpoint. + */ + explicit SearchJob(const Categories& searchCategories, + const QString& nextBatch = {}); + + // Result properties + + /// Describes which categories to search in and their criteria. + ResultCategories searchCategories() const + { + return loadFromJson<ResultCategories>("search_categories"_ls); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::IncludeEventContext> { + static void dumpTo(QJsonObject& jo, + const SearchJob::IncludeEventContext& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("before_limit"), + pod.beforeLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("after_limit"), pod.afterLimit); + addParam<IfNotEmpty>(jo, QStringLiteral("include_profile"), + pod.includeProfile); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Group> { + static void dumpTo(QJsonObject& jo, const SearchJob::Group& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("key"), pod.key); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Groupings> { + static void dumpTo(QJsonObject& jo, const SearchJob::Groupings& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("group_by"), pod.groupBy); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::RoomEventsCriteria> { + static void dumpTo(QJsonObject& jo, const SearchJob::RoomEventsCriteria& pod) + { + addParam<>(jo, QStringLiteral("search_term"), pod.searchTerm); + addParam<IfNotEmpty>(jo, QStringLiteral("keys"), pod.keys); + addParam<IfNotEmpty>(jo, QStringLiteral("filter"), pod.filter); + addParam<IfNotEmpty>(jo, QStringLiteral("order_by"), pod.orderBy); + addParam<IfNotEmpty>(jo, QStringLiteral("event_context"), + pod.eventContext); + addParam<IfNotEmpty>(jo, QStringLiteral("include_state"), + pod.includeState); + addParam<IfNotEmpty>(jo, QStringLiteral("groupings"), pod.groupings); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Categories> { + static void dumpTo(QJsonObject& jo, const SearchJob::Categories& pod) + { + addParam<IfNotEmpty>(jo, QStringLiteral("room_events"), pod.roomEvents); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::UserProfile> { + static void fillFrom(const QJsonObject& jo, SearchJob::UserProfile& result) + { + fromJson(jo.value("displayname"_ls), result.displayname); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::EventContext> { + static void fillFrom(const QJsonObject& jo, SearchJob::EventContext& result) + { + fromJson(jo.value("start"_ls), result.begin); + fromJson(jo.value("end"_ls), result.end); + fromJson(jo.value("profile_info"_ls), result.profileInfo); + fromJson(jo.value("events_before"_ls), result.eventsBefore); + fromJson(jo.value("events_after"_ls), result.eventsAfter); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::Result> { + static void fillFrom(const QJsonObject& jo, SearchJob::Result& result) + { + fromJson(jo.value("rank"_ls), result.rank); + fromJson(jo.value("result"_ls), result.result); + fromJson(jo.value("context"_ls), result.context); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::GroupValue> { + static void fillFrom(const QJsonObject& jo, SearchJob::GroupValue& result) + { + fromJson(jo.value("next_batch"_ls), result.nextBatch); + fromJson(jo.value("order"_ls), result.order); + fromJson(jo.value("results"_ls), result.results); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::ResultRoomEvents> { + static void fillFrom(const QJsonObject& jo, + SearchJob::ResultRoomEvents& result) + { + fromJson(jo.value("count"_ls), result.count); + fromJson(jo.value("highlights"_ls), result.highlights); + fromJson(jo.value("results"_ls), result.results); + fromJson(jo.value("state"_ls), result.state); + fromJson(jo.value("groups"_ls), result.groups); + fromJson(jo.value("next_batch"_ls), result.nextBatch); + } +}; + +template <> +struct JsonObjectConverter<SearchJob::ResultCategories> { + static void fillFrom(const QJsonObject& jo, + SearchJob::ResultCategories& result) + { + fromJson(jo.value("room_events"_ls), result.roomEvents); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/space_hierarchy.cpp b/lib/csapi/space_hierarchy.cpp new file mode 100644 index 00000000..7b5c7eac --- /dev/null +++ b/lib/csapi/space_hierarchy.cpp @@ -0,0 +1,43 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "space_hierarchy.h" + +using namespace Quotient; + +auto queryToGetSpaceHierarchy(Omittable<bool> suggestedOnly, + Omittable<int> limit, Omittable<int> maxDepth, + const QString& from) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("suggested_only"), suggestedOnly); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("max_depth"), maxDepth); + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + return _q; +} + +QUrl GetSpaceHierarchyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + Omittable<bool> suggestedOnly, + Omittable<int> limit, + Omittable<int> maxDepth, + const QString& from) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"), + queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from)); +} + +GetSpaceHierarchyJob::GetSpaceHierarchyJob(const QString& roomId, + Omittable<bool> suggestedOnly, + Omittable<int> limit, + Omittable<int> maxDepth, + const QString& from) + : BaseJob(HttpVerb::Get, QStringLiteral("GetSpaceHierarchyJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/hierarchy"), + queryToGetSpaceHierarchy(suggestedOnly, limit, maxDepth, from)) +{ + addExpectedKey("rooms"); +} diff --git a/lib/csapi/space_hierarchy.h b/lib/csapi/space_hierarchy.h new file mode 100644 index 00000000..e5da6df2 --- /dev/null +++ b/lib/csapi/space_hierarchy.h @@ -0,0 +1,152 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/stateevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Retrieve a portion of a space tree. + * + * Paginates over the space tree in a depth-first manner to locate child rooms + * of a given space. + * + * Where a child room is unknown to the local server, federation is used to fill + * in the details. The servers listed in the `via` array should be contacted to + * attempt to fill in missing rooms. + * + * Only [`m.space.child`](#mspacechild) state events of the room are considered. + * Invalid child rooms and parent events are not covered by this endpoint. + */ +class QUOTIENT_API GetSpaceHierarchyJob : public BaseJob { +public: + // Inner data structures + + /// Paginates over the space tree in a depth-first manner to locate child + /// rooms of a given space. + /// + /// Where a child room is unknown to the local server, federation is used to + /// fill in the details. The servers listed in the `via` array should be + /// contacted to attempt to fill in missing rooms. + /// + /// Only [`m.space.child`](#mspacechild) state events of the room are + /// considered. Invalid child rooms and parent events are not covered by + /// this endpoint. + struct ChildRoomsChunk { + /// The canonical alias of the room, if any. + QString canonicalAlias; + /// The name of the room, if any. + QString name; + /// The number of members joined to the room. + int numJoinedMembers; + /// The ID of the room. + QString roomId; + /// The topic of the room, if any. + QString topic; + /// Whether the room may be viewed by guest users without joining. + bool worldReadable; + /// Whether guest users may join the room and participate in it. + /// If they can, they will be subject to ordinary power level + /// rules like any other user. + bool guestCanJoin; + /// The URL for the room's avatar, if one is set. + QUrl avatarUrl; + /// The room's join rule. When not present, the room is assumed to + /// be `public`. + QString joinRule; + /// The `type` of room (from + /// [`m.room.create`](/client-server-api/#mroomcreate)), if any. + QString roomType; + /// The [`m.space.child`](#mspacechild) events of the space-room, + /// represented as [Stripped State Events](#stripped-state) with an + /// added `origin_server_ts` key. + /// + /// If the room is not a space-room, this should be empty. + StateEvents childrenState; + }; + + // Construction/destruction + + /*! \brief Retrieve a portion of a space tree. + * + * \param roomId + * The room ID of the space to get a hierarchy for. + * + * \param suggestedOnly + * Optional (default `false`) flag to indicate whether or not the server + * should only consider suggested rooms. Suggested rooms are annotated in + * their [`m.space.child`](#mspacechild) event contents. + * + * \param limit + * Optional limit for the maximum number of rooms to include per response. + * Must be an integer greater than zero. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param maxDepth + * Optional limit for how far to go into the space. Must be a non-negative + * integer. + * + * When reached, no further child rooms will be returned. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param from + * A pagination token from a previous result. If specified, `max_depth` + * and `suggested_only` cannot be changed from the first request. + */ + explicit GetSpaceHierarchyJob(const QString& roomId, + Omittable<bool> suggestedOnly = none, + Omittable<int> limit = none, + Omittable<int> maxDepth = none, + const QString& from = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetSpaceHierarchyJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + Omittable<bool> suggestedOnly = none, + Omittable<int> limit = none, + Omittable<int> maxDepth = none, + const QString& from = {}); + + // Result properties + + /// The rooms for the current page, with the current filters. + std::vector<ChildRoomsChunk> rooms() + { + return takeFromJson<std::vector<ChildRoomsChunk>>("rooms"_ls); + } + + /// A token to supply to `from` to keep paginating the responses. Not + /// present when there are no further results. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } +}; + +template <> +struct JsonObjectConverter<GetSpaceHierarchyJob::ChildRoomsChunk> { + static void fillFrom(const QJsonObject& jo, + GetSpaceHierarchyJob::ChildRoomsChunk& result) + { + fromJson(jo.value("canonical_alias"_ls), result.canonicalAlias); + fromJson(jo.value("name"_ls), result.name); + fromJson(jo.value("num_joined_members"_ls), result.numJoinedMembers); + fromJson(jo.value("room_id"_ls), result.roomId); + fromJson(jo.value("topic"_ls), result.topic); + fromJson(jo.value("world_readable"_ls), result.worldReadable); + fromJson(jo.value("guest_can_join"_ls), result.guestCanJoin); + fromJson(jo.value("avatar_url"_ls), result.avatarUrl); + fromJson(jo.value("join_rule"_ls), result.joinRule); + fromJson(jo.value("room_type"_ls), result.roomType); + fromJson(jo.value("children_state"_ls), result.childrenState); + } +}; + +} // namespace Quotient diff --git a/lib/csapi/sso_login_redirect.cpp b/lib/csapi/sso_login_redirect.cpp index 85a18560..71f8147c 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/v3", + "/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/v3", "/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/v3", + "/login/sso/redirect/", idpId), + queryToRedirectToIdP(redirectUrl)); +} + +RedirectToIdPJob::RedirectToIdPJob(const QString& idpId, + const QString& redirectUrl) + : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"), + makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId), + queryToRedirectToIdP(redirectUrl), {}, false) +{} diff --git a/lib/csapi/sso_login_redirect.h b/lib/csapi/sso_login_redirect.h index 6205ca59..f4f81c1e 100644 --- a/lib/csapi/sso_login_redirect.h +++ b/lib/csapi/sso_login_redirect.h @@ -17,7 +17,7 @@ namespace Quotient { * 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 { +class QUOTIENT_API RedirectToSSOJob : public BaseJob { public: /*! \brief Redirect the user's browser to the SSO interface. * @@ -35,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 QUOTIENT_API 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..2c85842d 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -4,49 +4,47 @@ #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/v3", "/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/v3", "/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/v3", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) { - QJsonObject _data; - fillJson(_data, additionalProperties); - addParam<IfNotEmpty>(_data, QStringLiteral("order"), order); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + fillJson(_dataJson, additionalProperties); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("order"), order); + setRequestData({ _dataJson }); } 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/v3", "/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/v3", "/user/", userId, "/rooms/", + roomId, "/tags/", tag)) {} diff --git a/lib/csapi/tags.h b/lib/csapi/tags.h index a854531a..f4250674 100644 --- a/lib/csapi/tags.h +++ b/lib/csapi/tags.h @@ -12,7 +12,7 @@ namespace Quotient { * * List the tags set by a user on a room. */ -class GetRoomTagsJob : public BaseJob { +class QUOTIENT_API GetRoomTagsJob : public BaseJob { public: // Inner data structures @@ -68,7 +68,7 @@ struct JsonObjectConverter<GetRoomTagsJob::Tag> { * * Add a tag to the room. */ -class SetRoomTagJob : public BaseJob { +class QUOTIENT_API SetRoomTagJob : public BaseJob { public: /*! \brief Add a tag to a room. * @@ -98,7 +98,7 @@ public: * * Remove a tag from the room. */ -class DeleteRoomTagJob : public BaseJob { +class QUOTIENT_API DeleteRoomTagJob : public BaseJob { public: /*! \brief Remove a tag from the room. * diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index baf1fab5..1e5870ce 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/v3", + "/thirdparty/protocols")); } GetProtocolsJob::GetProtocolsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocols") + makePath("/_matrix/client/v3", "/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/v3", + "/thirdparty/protocol/", protocol)); } GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/protocol/" - % protocol) + makePath("/_matrix/client/v3", "/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/v3", + "/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/v3", "/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/v3", + "/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/v3", "/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/v3", + "/thirdparty/location"), queryToQueryLocationByAlias(alias)); } QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/location", + makePath("/_matrix/client/v3", "/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/v3", + "/thirdparty/user"), queryToQueryUserByID(userid)); } QueryUserByIDJob::QueryUserByIDJob(const QString& userid) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"), - QStringLiteral("/_matrix/client/r0") % "/thirdparty/user", + makePath("/_matrix/client/v3", "/thirdparty/user"), queryToQueryUserByID(userid)) {} diff --git a/lib/csapi/third_party_lookup.h b/lib/csapi/third_party_lookup.h index 969e767c..30c5346e 100644 --- a/lib/csapi/third_party_lookup.h +++ b/lib/csapi/third_party_lookup.h @@ -18,7 +18,7 @@ namespace Quotient { * homeserver. Includes both the available protocols and all fields * required for queries against each protocol. */ -class GetProtocolsJob : public BaseJob { +class QUOTIENT_API GetProtocolsJob : public BaseJob { public: /// Retrieve metadata about all protocols that a homeserver supports. explicit GetProtocolsJob(); @@ -45,7 +45,7 @@ public: * Fetches the metadata from the homeserver about a particular third party * protocol. */ -class GetProtocolMetadataJob : public BaseJob { +class QUOTIENT_API GetProtocolMetadataJob : public BaseJob { public: /*! \brief Retrieve metadata about a specific protocol that the homeserver * supports. @@ -82,7 +82,7 @@ public: * identifier. It should attempt to canonicalise the identifier as much * as reasonably possible given the network type. */ -class QueryLocationByProtocolJob : public BaseJob { +class QUOTIENT_API QueryLocationByProtocolJob : public BaseJob { public: /*! \brief Retrieve Matrix-side portals rooms leading to a third party * location. @@ -119,7 +119,7 @@ public: * Retrieve a Matrix User ID linked to a user on the third party service, given * a set of user parameters. */ -class QueryUserByProtocolJob : public BaseJob { +class QUOTIENT_API QueryUserByProtocolJob : public BaseJob { public: /*! \brief Retrieve the Matrix User ID of a corresponding third party user. * @@ -155,7 +155,7 @@ public: * Retrieve an array of third party network locations from a Matrix room * alias. */ -class QueryLocationByAliasJob : public BaseJob { +class QUOTIENT_API QueryLocationByAliasJob : public BaseJob { public: /*! \brief Reverse-lookup third party locations given a Matrix room alias. * @@ -184,7 +184,7 @@ public: * * Retrieve an array of third party users from a Matrix User ID. */ -class QueryUserByIDJob : public BaseJob { +class QUOTIENT_API QueryUserByIDJob : public BaseJob { public: /*! \brief Reverse-lookup third party users given a Matrix User ID. * diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index fda772d2..3ca986c7 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -4,21 +4,18 @@ #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/v3", "/rooms/", roomId, "/invite")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("id_server"), idServer); - addParam<>(_data, QStringLiteral("id_access_token"), idAccessToken); - addParam<>(_data, QStringLiteral("medium"), medium); - addParam<>(_data, QStringLiteral("address"), address); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("id_server"), idServer); + addParam<>(_dataJson, QStringLiteral("id_access_token"), idAccessToken); + addParam<>(_dataJson, QStringLiteral("medium"), medium); + addParam<>(_dataJson, QStringLiteral("address"), address); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/third_party_membership.h b/lib/csapi/third_party_membership.h index a424678f..1129a9a8 100644 --- a/lib/csapi/third_party_membership.h +++ b/lib/csapi/third_party_membership.h @@ -16,7 +16,7 @@ namespace Quotient { * 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](/client-server-api/#post_matrixclientr0roomsroomidinvite). + * section](/client-server-api/#post_matrixclientv3roomsroomidinvite). * * This API invites a user to participate in a particular room. * They do not start participating in the room until they actually join the @@ -52,7 +52,7 @@ namespace Quotient { * If a token is requested from the identity server, the homeserver will * append a `m.room.third_party_invite` event to the room. */ -class InviteBy3PIDJob : public BaseJob { +class QUOTIENT_API InviteBy3PIDJob : public BaseJob { public: /*! \brief Invite a user to participate in a particular room. * diff --git a/lib/csapi/threads_list.cpp b/lib/csapi/threads_list.cpp new file mode 100644 index 00000000..26924f24 --- /dev/null +++ b/lib/csapi/threads_list.cpp @@ -0,0 +1,37 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#include "threads_list.h" + +using namespace Quotient; + +auto queryToGetThreadRoots(const QString& include, Omittable<int> limit, + const QString& from) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("include"), include); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + return _q; +} + +QUrl GetThreadRootsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& include, + Omittable<int> limit, const QString& from) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", + roomId, "/threads"), + queryToGetThreadRoots(include, limit, from)); +} + +GetThreadRootsJob::GetThreadRootsJob(const QString& roomId, + const QString& include, + Omittable<int> limit, const QString& from) + : BaseJob(HttpVerb::Get, QStringLiteral("GetThreadRootsJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/threads"), + queryToGetThreadRoots(include, limit, from)) +{ + addExpectedKey("chunk"); +} diff --git a/lib/csapi/threads_list.h b/lib/csapi/threads_list.h new file mode 100644 index 00000000..7041583a --- /dev/null +++ b/lib/csapi/threads_list.h @@ -0,0 +1,76 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/roomevent.h" +#include "jobs/basejob.h" + +namespace Quotient { + +/*! \brief Retrieve a list of threads in a room, with optional filters. + * + * Paginates over the thread roots in a room, ordered by the `latest_event` of + * each thread root in its bundle. + */ +class QUOTIENT_API GetThreadRootsJob : public BaseJob { +public: + /*! \brief Retrieve a list of threads in a room, with optional filters. + * + * \param roomId + * The room ID where the thread roots are located. + * + * \param include + * Optional (default `all`) flag to denote which thread roots are of + * interest to the caller. When `all`, all thread roots found in the room + * are returned. When `participated`, only thread roots for threads the user + * has [participated + * in](/client-server-api/#server-side-aggreagtion-of-mthread-relationships) + * will be returned. + * + * \param limit + * Optional limit for the maximum number of thread roots to include per + * response. Must be an integer greater than zero. + * + * Servers should apply a default value, and impose a maximum value to + * avoid resource exhaustion. + * + * \param from + * A pagination token from a previous result. When not provided, the + * server starts paginating from the most recent event visible to the user + * (as per history visibility rules; topologically). + */ + explicit GetThreadRootsJob(const QString& roomId, + const QString& include = {}, + Omittable<int> limit = none, + const QString& from = {}); + + /*! \brief Construct a URL without creating a full-fledged job object + * + * This function can be used when a URL for GetThreadRootsJob + * is necessary but the job itself isn't. + */ + static QUrl makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& include = {}, + Omittable<int> limit = none, + const QString& from = {}); + + // Result properties + + /// The thread roots, ordered by the `latest_event` in each event's + /// aggregation bundle. All events returned include bundled + /// [aggregations](/client-server-api/#aggregations). + /// + /// If the thread root event was sent by an [ignored + /// user](/client-server-api/#ignoring-users), the event is returned + /// redacted to the caller. This is to simulate the same behaviour of a + /// client doing aggregation locally on the thread. + RoomEvents chunk() { return takeFromJson<RoomEvents>("chunk"_ls); } + + /// A token to supply to `from` to keep paginating the responses. Not + /// present when there are no further results. + QString nextBatch() const { return loadFromJson<QString>("next_batch"_ls); } +}; + +} // namespace Quotient diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 28c4115a..e10fac69 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/v3", "/sendToDevice/", eventType, "/", + txnId)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("messages"), messages); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("messages"), messages); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/to_device.h b/lib/csapi/to_device.h index f5d69d65..54828337 100644 --- a/lib/csapi/to_device.h +++ b/lib/csapi/to_device.h @@ -13,7 +13,7 @@ namespace Quotient { * This endpoint is used to send send-to-device events to a set of * client devices. */ -class SendToDeviceJob : public BaseJob { +class QUOTIENT_API SendToDeviceJob : public BaseJob { public: /*! \brief Send an event to a given set of devices. * @@ -21,9 +21,10 @@ public: * The type of event to send. * * \param txnId - * The transaction ID for this event. Clients should generate an - * ID unique across requests with the same access token; it will be - * used by the server to ensure idempotency of requests. + * The [transaction ID](/client-server-api/#transaction-identifiers) for + * this event. Clients should generate an ID unique across requests with the + * same access token; it will be used by the server to ensure idempotency of + * requests. * * \param messages * The messages to send. A map from user ID, to a map from @@ -32,7 +33,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..21bd45ae 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -4,18 +4,16 @@ #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/v3", "/rooms/", roomId, "/typing/", + userId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("typing"), typing); - addParam<IfNotEmpty>(_data, QStringLiteral("timeout"), timeout); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("typing"), typing); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("timeout"), timeout); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/typing.h b/lib/csapi/typing.h index 64a310d0..234e91b0 100644 --- a/lib/csapi/typing.h +++ b/lib/csapi/typing.h @@ -15,7 +15,7 @@ namespace Quotient { * Alternatively, if `typing` is `false`, it tells the server that the * user has stopped typing. */ -class SetTypingJob : public BaseJob { +class QUOTIENT_API SetTypingJob : public BaseJob { public: /*! \brief Informs the server that the user has started or stopped typing. * diff --git a/lib/csapi/users.cpp b/lib/csapi/users.cpp index a0279d7e..c65280ee 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -4,19 +4,17 @@ #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/v3", "/user_directory/search")) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("search_term"), searchTerm); - addParam<IfNotEmpty>(_data, QStringLiteral("limit"), limit); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("search_term"), searchTerm); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("limit"), limit); + setRequestData({ _dataJson }); addExpectedKey("results"); addExpectedKey("limited"); } diff --git a/lib/csapi/users.h b/lib/csapi/users.h index eab18f6c..3c99758b 100644 --- a/lib/csapi/users.h +++ b/lib/csapi/users.h @@ -21,7 +21,7 @@ namespace Quotient { * names preferably using a collation determined based upon the * `Accept-Language` header provided in the request, if present. */ -class SearchUserDirectoryJob : public BaseJob { +class QUOTIENT_API SearchUserDirectoryJob : public BaseJob { public: // Inner data structures @@ -41,7 +41,7 @@ public: /// 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 896e2ea9..9f799cb0 100644 --- a/lib/csapi/versions.h +++ b/lib/csapi/versions.h @@ -12,11 +12,9 @@ namespace Quotient { * * Gets the versions of the specification supported by the server. * - * Values will take the form `rX.Y.Z`. - * - * Only the latest `Z` value will be reported for each supported `X.Y` value. - * i.e. if the server implements `r0.0.0`, `r0.0.1`, and `r1.2.0`, it will - * report `r0.0.1` and `r1.2.0`. + * Values will take the form `vX.Y` or `rX.Y.Z` in historical cases. See + * [the Specification Versioning](../#specification-versions) for more + * information. * * The server may additionally advertise experimental features it supports * through `unstable_features`. These features should be namespaced and @@ -31,7 +29,7 @@ namespace Quotient { * upgrade appropriately. Additionally, clients should avoid using unstable * features in their stable releases. */ -class GetVersionsJob : public BaseJob { +class QUOTIENT_API GetVersionsJob : public BaseJob { public: /// Gets the versions of the specification supported by the server. explicit GetVersionsJob(); diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index 43170057..1e1f2441 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/v3", "/voip/turnServer")); } GetTurnServerJob::GetTurnServerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"), - QStringLiteral("/_matrix/client/r0") % "/voip/turnServer") + makePath("/_matrix/client/v3", "/voip/turnServer")) {} diff --git a/lib/csapi/voip.h b/lib/csapi/voip.h index 087ebbbd..38904f60 100644 --- a/lib/csapi/voip.h +++ b/lib/csapi/voip.h @@ -13,7 +13,7 @@ namespace Quotient { * This API provides credentials for the client to use when initiating * calls. */ -class GetTurnServerJob : public BaseJob { +class QUOTIENT_API GetTurnServerJob : public BaseJob { public: /// Obtain TURN server credentials. explicit GetTurnServerJob(); 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 c707d232..8615191c 100644 --- a/lib/csapi/wellknown.h +++ b/lib/csapi/wellknown.h @@ -21,7 +21,7 @@ namespace Quotient { * Note that this endpoint is not necessarily handled by the homeserver, * but by another webserver, to be used for discovering the homeserver URL. */ -class GetWellknownJob : public BaseJob { +class QUOTIENT_API GetWellknownJob : public BaseJob { public: /// Gets Matrix server discovery information about the domain. explicit GetWellknownJob(); diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index 73f0298e..af0c5d31 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/v3", "/account/whoami")); } GetTokenOwnerJob::GetTokenOwnerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"), - QStringLiteral("/_matrix/client/r0") % "/account/whoami") + makePath("/_matrix/client/v3", "/account/whoami")) { addExpectedKey("user_id"); } diff --git a/lib/csapi/whoami.h b/lib/csapi/whoami.h index 184459ea..3451dbc3 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -19,7 +19,7 @@ namespace Quotient { * is registered by the appservice, and return it in the response * body. */ -class GetTokenOwnerJob : public BaseJob { +class QUOTIENT_API GetTokenOwnerJob : public BaseJob { public: /// Gets information about the owner of an access token. explicit GetTokenOwnerJob(); @@ -35,6 +35,20 @@ public: /// 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); } + + /// When `true`, the user is a [Guest User](#guest-access). When + /// not present or `false`, the user is presumed to be a non-guest + /// user. + Omittable<bool> isGuest() const + { + return loadFromJson<Omittable<bool>>("is_guest"_ls); + } }; } // namespace Quotient diff --git a/lib/database.cpp b/lib/database.cpp new file mode 100644 index 00000000..2b472648 --- /dev/null +++ b/lib/database.cpp @@ -0,0 +1,419 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "database.h" + +#include <QtSql/QSqlDatabase> +#include <QtSql/QSqlQuery> +#include <QtSql/QSqlError> +#include <QtCore/QStandardPaths> +#include <QtCore/QDebug> +#include <QtCore/QDir> + +#include "e2ee/e2ee.h" +#include "e2ee/qolmsession.h" +#include "e2ee/qolminboundsession.h" +#include "e2ee/qolmoutboundsession.h" + +using namespace Quotient; +Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) + : QObject(parent) + , m_matrixId(matrixId) +{ + m_matrixId.replace(':', '_'); + QSqlDatabase::addDatabase(QStringLiteral("QSQLITE"), QStringLiteral("Quotient_%1").arg(m_matrixId)); + QString databasePath = QStandardPaths::writableLocation(QStandardPaths::AppDataLocation) + QStringLiteral("/%1").arg(m_matrixId); + QDir(databasePath).mkpath(databasePath); + database().setDatabaseName(databasePath + QStringLiteral("/quotient_%1.db3").arg(deviceId)); + database().open(); + + switch(version()) { + case 0: migrateTo1(); [[fallthrough]]; + case 1: migrateTo2(); [[fallthrough]]; + case 2: migrateTo3(); [[fallthrough]]; + case 3: migrateTo4(); [[fallthrough]]; + case 4: migrateTo5(); + } +} + +int Database::version() +{ + auto query = execute(QStringLiteral("PRAGMA user_version;")); + if (query.next()) { + bool ok = false; + int value = query.value(0).toInt(&ok); + qCDebug(DATABASE) << "Database version" << value; + if (ok) + return value; + } else { + qCritical() << "Failed to check database version"; + } + return -1; +} + +QSqlQuery Database::execute(const QString &queryString) +{ + auto query = database().exec(queryString); + if (query.lastError().type() != QSqlError::NoError) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +QSqlQuery Database::execute(QSqlQuery &query) +{ + if (!query.exec()) { + qCritical() << "Failed to execute query"; + qCritical() << query.lastQuery(); + qCritical() << query.lastError(); + } + return query; +} + +void Database::transaction() +{ + database().transaction(); +} + +void Database::commit() +{ + database().commit(); +} + +void Database::migrateTo1() +{ + qCDebug(DATABASE) << "Migrating database to version 1"; + transaction(); + execute(QStringLiteral("CREATE TABLE accounts (pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE olm_sessions (senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE outbound_megolm_sessions (roomId TEXT, senderKey TEXT, sessionId TEXT, pickle TEXT);")); + execute(QStringLiteral("CREATE TABLE group_session_record_index (roomId TEXT, sessionId TEXT, i INTEGER, eventId TEXT, ts INTEGER);")); + execute(QStringLiteral("CREATE TABLE tracked_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE outdated_users (matrixId TEXT);")); + execute(QStringLiteral("CREATE TABLE tracked_devices (matrixId TEXT, deviceId TEXT, curveKeyId TEXT, curveKey TEXT, edKeyId TEXT, edKey TEXT);")); + + execute(QStringLiteral("PRAGMA user_version = 1;")); + commit(); +} + +void Database::migrateTo2() +{ + qCDebug(DATABASE) << "Migrating database to version 2"; + transaction(); + + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD ed25519Key TEXT")); + execute(QStringLiteral("ALTER TABLE olm_sessions ADD lastReceived TEXT")); + + // Add indexes for improving queries speed on larger database + execute(QStringLiteral("CREATE INDEX sessions_session_idx ON olm_sessions(sessionId)")); + execute(QStringLiteral("CREATE INDEX outbound_room_idx ON outbound_megolm_sessions(roomId)")); + execute(QStringLiteral("CREATE INDEX inbound_room_idx ON inbound_megolm_sessions(roomId)")); + execute(QStringLiteral("CREATE INDEX group_session_idx ON group_session_record_index(roomId, sessionId, i)")); + execute(QStringLiteral("PRAGMA user_version = 2;")); + commit(); +} + +void Database::migrateTo3() +{ + qCDebug(DATABASE) << "Migrating database to version 3"; + transaction(); + + execute(QStringLiteral("CREATE TABLE inbound_megolm_sessions_temp AS SELECT roomId, sessionId, pickle FROM inbound_megolm_sessions;")); + execute(QStringLiteral("DROP TABLE inbound_megolm_sessions;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions_temp RENAME TO inbound_megolm_sessions;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD olmSessionId TEXT;")); + execute(QStringLiteral("ALTER TABLE inbound_megolm_sessions ADD senderId TEXT;")); + execute(QStringLiteral("PRAGMA user_version = 3;")); + commit(); +} + +void Database::migrateTo4() +{ + qCDebug(DATABASE) << "Migrating database to version 4"; + transaction(); + + execute(QStringLiteral("CREATE TABLE sent_megolm_sessions (roomId TEXT, userId TEXT, deviceId TEXT, identityKey TEXT, sessionId TEXT, i INTEGER);")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD creationTime TEXT;")); + execute(QStringLiteral("ALTER TABLE outbound_megolm_sessions ADD messageCount INTEGER;")); + execute(QStringLiteral("PRAGMA user_version = 4;")); + commit(); +} + +void Database::migrateTo5() +{ + qCDebug(DATABASE) << "Migrating database to version 5"; + transaction(); + + execute(QStringLiteral("ALTER TABLE tracked_devices ADD verified BOOL;")); + execute(QStringLiteral("PRAGMA user_version = 5")); + commit(); +} + +QByteArray Database::accountPickle() +{ + auto query = prepareQuery(QStringLiteral("SELECT pickle FROM accounts;")); + execute(query); + if (query.next()) { + return query.value(QStringLiteral("pickle")).toByteArray(); + } + return {}; +} + +void Database::setAccountPickle(const QByteArray &pickle) +{ + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto query = prepareQuery(QStringLiteral("INSERT INTO accounts(pickle) VALUES(:pickle);")); + query.bindValue(":pickle", pickle); + transaction(); + execute(deleteQuery); + execute(query); + commit(); +} + +void Database::clear() +{ + auto query = prepareQuery(QStringLiteral("DELETE FROM accounts;")); + auto sessionsQuery = prepareQuery(QStringLiteral("DELETE FROM olm_sessions;")); + auto megolmSessionsQuery = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions;")); + auto groupSessionIndexRecordQuery = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index;")); + + transaction(); + execute(query); + execute(sessionsQuery); + execute(megolmSessionsQuery); + execute(groupSessionIndexRecordQuery); + commit(); + +} + +void Database::saveOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray &pickle, const QDateTime& timestamp) +{ + auto query = prepareQuery(QStringLiteral("INSERT INTO olm_sessions(senderKey, sessionId, pickle, lastReceived) VALUES(:senderKey, :sessionId, :pickle, :lastReceived);")); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + query.bindValue(":lastReceived", timestamp); + transaction(); + execute(query); + commit(); +} + +UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM olm_sessions ORDER BY lastReceived DESC;")); + transaction(); + execute(query); + commit(); + UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions; + while (query.next()) { + if (auto expectedSession = + QOlmSession::unpickle(query.value("pickle").toByteArray(), + picklingMode)) { + sessions[query.value("senderKey").toString()].emplace_back( + std::move(*expectedSession)); + } else + qCWarning(E2EE) + << "Failed to unpickle olm session:" << expectedSession.error(); + } + return sessions; +} + +UnorderedMap<QString, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM inbound_megolm_sessions WHERE roomId=:roomId;")); + query.bindValue(":roomId", roomId); + transaction(); + execute(query); + commit(); + UnorderedMap<QString, QOlmInboundGroupSessionPtr> sessions; + while (query.next()) { + if (auto expectedSession = QOlmInboundGroupSession::unpickle( + query.value("pickle").toByteArray(), picklingMode)) { + auto& sessionPtr = sessions[query.value("sessionId").toString()] = + std::move(*expectedSession); + sessionPtr->setOlmSessionId(query.value("olmSessionId").toString()); + sessionPtr->setSenderId(query.value("senderId").toString()); + } else + qCWarning(E2EE) << "Failed to unpickle megolm session:" + << expectedSession.error(); + } + return sessions; +} + +void Database::saveMegolmSession(const QString& roomId, const QString& sessionId, const QByteArray& pickle, const QString& senderId, const QString& olmSessionId) +{ + auto query = prepareQuery(QStringLiteral("INSERT INTO inbound_megolm_sessions(roomId, sessionId, pickle, senderId, olmSessionId) VALUES(:roomId, :sessionId, :pickle, :senderId, :olmSessionId);")); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":pickle", pickle); + query.bindValue(":senderId", senderId); + query.bindValue(":olmSessionId", olmSessionId); + transaction(); + execute(query); + commit(); +} + +void Database::addGroupSessionIndexRecord(const QString& roomId, const QString& sessionId, uint32_t index, const QString& eventId, qint64 ts) +{ + auto query = prepareQuery("INSERT INTO group_session_record_index(roomId, sessionId, i, eventId, ts) VALUES(:roomId, :sessionId, :index, :eventId, :ts);"); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + query.bindValue(":eventId", eventId); + query.bindValue(":ts", ts); + transaction(); + execute(query); + commit(); +} + +std::pair<QString, qint64> Database::groupSessionIndexRecord(const QString& roomId, const QString& sessionId, qint64 index) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM group_session_record_index WHERE roomId=:roomId AND sessionId=:sessionId AND i=:index;")); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + query.bindValue(":index", index); + transaction(); + execute(query); + commit(); + if (!query.next()) { + return {}; + } + return {query.value("eventId").toString(), query.value("ts").toLongLong()}; +} + +QSqlDatabase Database::database() +{ + return QSqlDatabase::database(QStringLiteral("Quotient_%1").arg(m_matrixId)); +} + +QSqlQuery Database::prepareQuery(const QString& queryString) +{ + QSqlQuery query(database()); + query.prepare(queryString); + return query; +} + +void Database::clearRoomData(const QString& roomId) +{ + auto query = prepareQuery(QStringLiteral("DELETE FROM inbound_megolm_sessions WHERE roomId=:roomId;")); + auto query2 = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId;")); + auto query3 = prepareQuery(QStringLiteral("DELETE FROM group_session_record_index WHERE roomId=:roomId;")); + transaction(); + execute(query); + execute(query2); + execute(query3); + commit(); +} + +void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTime& timestamp) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET lastReceived=:lastReceived WHERE sessionId=:sessionId;")); + query.bindValue(":lastReceived", timestamp); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} + +void Database::saveCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode, + const QOlmOutboundGroupSession& session) +{ + const auto pickle = session.pickle(picklingMode); + auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); + deleteQuery.bindValue(":roomId", roomId); + deleteQuery.bindValue(":sessionId", session.sessionId()); + + auto insertQuery = prepareQuery(QStringLiteral("INSERT INTO outbound_megolm_sessions(roomId, sessionId, pickle, creationTime, messageCount) VALUES(:roomId, :sessionId, :pickle, :creationTime, :messageCount);")); + insertQuery.bindValue(":roomId", roomId); + insertQuery.bindValue(":sessionId", session.sessionId()); + insertQuery.bindValue(":pickle", pickle); + insertQuery.bindValue(":creationTime", session.creationTime()); + insertQuery.bindValue(":messageCount", session.messageCount()); + + transaction(); + execute(deleteQuery); + execute(insertQuery); + commit(); +} + +QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode) +{ + auto query = prepareQuery(QStringLiteral("SELECT * FROM outbound_megolm_sessions WHERE roomId=:roomId ORDER BY creationTime DESC;")); + query.bindValue(":roomId", roomId); + execute(query); + if (query.next()) { + auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); + if (sessionResult) { + auto session = std::move(*sessionResult); + session->setCreationTime(query.value("creationTime").toDateTime()); + session->setMessageCount(query.value("messageCount").toInt()); + return session; + } + } + return nullptr; +} + +void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index) +{ + transaction(); + for (const auto& [user, device, curveKey] : devices) { + auto query = prepareQuery(QStringLiteral("INSERT INTO sent_megolm_sessions(roomId, userId, deviceId, identityKey, sessionId, i) VALUES(:roomId, :userId, :deviceId, :identityKey, :sessionId, :i);")); + query.bindValue(":roomId", roomId); + query.bindValue(":userId", user); + query.bindValue(":deviceId", device); + query.bindValue(":identityKey", curveKey); + query.bindValue(":sessionId", sessionId); + query.bindValue(":i", index); + execute(query); + } + commit(); +} + +QMultiHash<QString, QString> Database::devicesWithoutKey( + const QString& roomId, QMultiHash<QString, QString> devices, + const QString& sessionId) +{ + auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); + query.bindValue(":roomId", roomId); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); + while (query.next()) { + devices.remove(query.value("userId").toString(), + query.value("deviceId").toString()); + } + return devices; +} + +void Database::updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle) +{ + auto query = prepareQuery(QStringLiteral("UPDATE olm_sessions SET pickle=:pickle WHERE senderKey=:senderKey AND sessionId=:sessionId;")); + query.bindValue(":pickle", pickle); + query.bindValue(":senderKey", senderKey); + query.bindValue(":sessionId", sessionId); + transaction(); + execute(query); + commit(); +} + +void Database::setSessionVerified(const QString& edKeyId) +{ + auto query = prepareQuery(QStringLiteral("UPDATE tracked_devices SET verified=true WHERE edKeyId=:edKeyId;")); + query.bindValue(":edKeyId", edKeyId); + transaction(); + execute(query); + commit(); +} + +bool Database::isSessionVerified(const QString& edKey) +{ + auto query = prepareQuery(QStringLiteral("SELECT verified FROM tracked_devices WHERE edKey=:edKey")); + query.bindValue(":edKey", edKey); + execute(query); + return query.next() && query.value("verified").toBool(); +} diff --git a/lib/database.h b/lib/database.h new file mode 100644 index 00000000..8a133f8e --- /dev/null +++ b/lib/database.h @@ -0,0 +1,81 @@ +// SPDX-FileCopyrightText: 2021 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QtCore/QObject> +#include <QtSql/QSqlQuery> +#include <QtCore/QVector> + +#include <QtCore/QHash> + +#include "e2ee/e2ee.h" + +namespace Quotient { + +class QUOTIENT_API Database : public QObject +{ + Q_OBJECT +public: + Database(const QString& matrixId, const QString& deviceId, QObject* parent); + + int version(); + void transaction(); + void commit(); + QSqlQuery execute(const QString &queryString); + QSqlQuery execute(QSqlQuery &query); + QSqlDatabase database(); + QSqlQuery prepareQuery(const QString& quaryString); + + QByteArray accountPickle(); + void setAccountPickle(const QByteArray &pickle); + void clear(); + void saveOlmSession(const QString& senderKey, const QString& sessionId, + const QByteArray& pickle, const QDateTime& timestamp); + UnorderedMap<QString, std::vector<QOlmSessionPtr>> loadOlmSessions( + const PicklingMode& picklingMode); + UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadMegolmSessions( + const QString& roomId, const PicklingMode& picklingMode); + void saveMegolmSession(const QString& roomId, const QString& sessionId, + const QByteArray& pickle, const QString& senderId, + const QString& olmSessionId); + void addGroupSessionIndexRecord(const QString& roomId, + const QString& sessionId, uint32_t index, + const QString& eventId, qint64 ts); + std::pair<QString, qint64> groupSessionIndexRecord(const QString& roomId, + const QString& sessionId, + qint64 index); + void clearRoomData(const QString& roomId); + void setOlmSessionLastReceived(const QString& sessionId, + const QDateTime& timestamp); + QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode); + void saveCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode, + const QOlmOutboundGroupSession& session); + void updateOlmSession(const QString& senderKey, const QString& sessionId, + const QByteArray& pickle); + + // Returns a map UserId -> [DeviceId] that have not received key yet + QMultiHash<QString, QString> devicesWithoutKey( + const QString& roomId, QMultiHash<QString, QString> devices, + const QString& sessionId); + // 'devices' contains tuples {userId, deviceId, curveKey} + void setDevicesReceivedKey( + const QString& roomId, + const QVector<std::tuple<QString, QString, QString>>& devices, + const QString& sessionId, int index); + + bool isSessionVerified(const QString& edKey); + void setSessionVerified(const QString& edKeyId); + +private: + void migrateTo1(); + void migrateTo2(); + void migrateTo3(); + void migrateTo4(); + void migrateTo5(); + + QString m_matrixId; +}; +} // namespace Quotient diff --git a/lib/e2ee.h b/lib/e2ee.h deleted file mode 100644 index f49b9748..00000000 --- a/lib/e2ee.h +++ /dev/null @@ -1,31 +0,0 @@ -#pragma once - -#include "util.h" - -#include <QtCore/QStringList> - -namespace Quotient { -inline const auto CiphertextKeyL = "ciphertext"_ls; -inline const auto SenderKeyKeyL = "sender_key"_ls; -inline const auto DeviceIdKeyL = "device_id"_ls; -inline const auto SessionIdKeyL = "session_id"_ls; - -inline const auto AlgorithmKeyL = "algorithm"_ls; -inline const auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; -inline const auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; - -inline const auto AlgorithmKey = QStringLiteral("algorithm"); -inline const auto RotationPeriodMsKey = QStringLiteral("rotation_period_ms"); -inline const auto RotationPeriodMsgsKey = - QStringLiteral("rotation_period_msgs"); - -inline const auto Ed25519Key = QStringLiteral("ed25519"); -inline const auto Curve25519Key = QStringLiteral("curve25519"); -inline const auto SignedCurve25519Key = QStringLiteral("signed_curve25519"); -inline const auto OlmV1Curve25519AesSha2AlgoKey = - QStringLiteral("m.olm.v1.curve25519-aes-sha2"); -inline const auto MegolmV1AesSha2AlgoKey = - QStringLiteral("m.megolm.v1.aes-sha2"); -inline const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoKey, - MegolmV1AesSha2AlgoKey }; -} // namespace Quotient diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h new file mode 100644 index 00000000..5999c0be --- /dev/null +++ b/lib/e2ee/e2ee.h @@ -0,0 +1,139 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +#include <QtCore/QMetaType> +#include <QtCore/QStringBuilder> + +#include <array> + +#ifdef Quotient_E2EE_ENABLED +# include "expected.h" + +# include <olm/error.h> +# include <variant> +#endif + +namespace Quotient { + +constexpr auto AlgorithmKeyL = "algorithm"_ls; +constexpr auto RotationPeriodMsKeyL = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKeyL = "rotation_period_msgs"_ls; + +constexpr auto AlgorithmKey = "algorithm"_ls; +constexpr auto RotationPeriodMsKey = "rotation_period_ms"_ls; +constexpr auto RotationPeriodMsgsKey = "rotation_period_msgs"_ls; + +constexpr auto Ed25519Key = "ed25519"_ls; +constexpr auto Curve25519Key = "curve25519"_ls; +constexpr auto SignedCurve25519Key = "signed_curve25519"_ls; + +constexpr auto OlmV1Curve25519AesSha2AlgoKey = "m.olm.v1.curve25519-aes-sha2"_ls; +constexpr auto MegolmV1AesSha2AlgoKey = "m.megolm.v1.aes-sha2"_ls; + +constexpr std::array SupportedAlgorithms { OlmV1Curve25519AesSha2AlgoKey, + MegolmV1AesSha2AlgoKey }; + +inline bool isSupportedAlgorithm(const QString& algorithm) +{ + return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), + algorithm) + != SupportedAlgorithms.cend(); +} + +#ifdef Quotient_E2EE_ENABLED +struct Unencrypted {}; +struct Encrypted { + QByteArray key; +}; + +using PicklingMode = std::variant<Unencrypted, Encrypted>; + +class QOlmSession; +using QOlmSessionPtr = std::unique_ptr<QOlmSession>; + +class QOlmInboundGroupSession; +using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>; + +class QOlmOutboundGroupSession; +using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>; + +template <typename T> +using QOlmExpected = Expected<T, OlmErrorCode>; +#endif + +struct IdentityKeys +{ + QByteArray curve25519; + QByteArray ed25519; +}; + +//! Struct representing the one-time keys. +struct UnsignedOneTimeKeys +{ + QHash<QString, QHash<QString, QString>> keys; + + //! Get the HashMap containing the curve25519 one-time keys. + QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; } +}; + +class SignedOneTimeKey { +public: + explicit SignedOneTimeKey(const QString& unsignedKey, const QString& userId, + const QString& deviceId, + const QByteArray& signature) + : payload { { "key"_ls, unsignedKey }, + { "signatures"_ls, + QJsonObject { + { userId, QJsonObject { { "ed25519:"_ls % deviceId, + QString(signature) } } } } } } + {} + explicit SignedOneTimeKey(const QJsonObject& jo = {}) + : payload(jo) + {} + + //! Unpadded Base64-encoded 32-byte Curve25519 public key + QByteArray key() const { return payload["key"_ls].toString().toLatin1(); } + + //! \brief Signatures of the key object + //! + //! The signature is calculated using the process described at + //! https://spec.matrix.org/v1.3/appendices/#signing-json + auto signatures() const + { + return fromJson<QHash<QString, QHash<QString, QString>>>( + payload["signatures"_ls]); + } + + QByteArray signature(QStringView userId, QStringView deviceId) const + { + return payload["signatures"_ls][userId]["ed25519:"_ls % deviceId] + .toString() + .toLatin1(); + } + + //! Whether the key is a fallback key + bool isFallback() const { return payload["fallback"_ls].toBool(); } + auto toJson() const { return payload; } + auto toJsonForVerification() const + { + auto json = payload; + json.remove("signatures"_ls); + json.remove("unsigned"_ls); + return QJsonDocument(json).toJson(QJsonDocument::Compact); + } + +private: + QJsonObject payload; +}; + +using OneTimeKeys = QHash<QString, std::variant<QString, SignedOneTimeKey>>; + +} // namespace Quotient + +Q_DECLARE_METATYPE(Quotient::SignedOneTimeKey) diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp new file mode 100644 index 00000000..345ab16b --- /dev/null +++ b/lib/e2ee/qolmaccount.cpp @@ -0,0 +1,275 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmaccount.h" + +#include "connection.h" +#include "e2ee/qolmsession.h" +#include "e2ee/qolmutility.h" +#include "e2ee/qolmutils.h" + +#include "csapi/keys.h" + +#include <QtCore/QRandomGenerator> + +#include <olm/olm.h> + +using namespace Quotient; + +// Convert olm error to enum +OlmErrorCode QOlmAccount::lastErrorCode() const { + return olm_account_last_error_code(m_account); +} + +const char* QOlmAccount::lastError() const +{ + return olm_account_last_error(m_account); +} + +QOlmAccount::QOlmAccount(QStringView userId, QStringView deviceId, + QObject* parent) + : QObject(parent) + , m_userId(userId.toString()) + , m_deviceId(deviceId.toString()) +{} + +QOlmAccount::~QOlmAccount() +{ + olm_clear_account(m_account); + delete[](reinterpret_cast<uint8_t *>(m_account)); +} + +void QOlmAccount::createNewAccount() +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + if (const auto randomLength = olm_create_account_random_length(m_account); + olm_create_account(m_account, RandomBuffer(randomLength), randomLength) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to create a new account"); + + emit needsSave(); +} + +OlmErrorCode QOlmAccount::unpickle(QByteArray&& pickled, + const PicklingMode& mode) +{ + m_account = olm_account(new uint8_t[olm_account_size()]); + if (const auto key = toKey(mode); + olm_unpickle_account(m_account, key.data(), key.length(), + pickled.data(), pickled.size()) + == olm_error()) { + // Probably log the user out since we have no way of getting to the keys + return lastErrorCode(); + } + return OLM_SUCCESS; +} + +QByteArray QOlmAccount::pickle(const PicklingMode &mode) +{ + const QByteArray key = toKey(mode); + const size_t pickleLength = olm_pickle_account_length(m_account); + QByteArray pickleBuffer(pickleLength, '\0'); + if (olm_pickle_account(m_account, key.data(), key.length(), + pickleBuffer.data(), pickleLength) + == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable("Failed to pickle Olm account " + + accountId())); + + return pickleBuffer; +} + +IdentityKeys QOlmAccount::identityKeys() const +{ + const size_t keyLength = olm_account_identity_keys_length(m_account); + QByteArray keyBuffer(keyLength, '\0'); + if (olm_account_identity_keys(m_account, keyBuffer.data(), keyLength) + == olm_error()) { + QOLM_INTERNAL_ERROR( + qPrintable("Failed to get " % accountId() % " identity keys")); + } + const auto key = QJsonDocument::fromJson(keyBuffer).object(); + return IdentityKeys { + key.value(QStringLiteral("curve25519")).toString().toUtf8(), + key.value(QStringLiteral("ed25519")).toString().toUtf8() + }; +} + +QByteArray QOlmAccount::sign(const QByteArray &message) const +{ + QByteArray signatureBuffer(olm_account_signature_length(m_account), '\0'); + + if (olm_account_sign(m_account, message.data(), message.length(), + signatureBuffer.data(), signatureBuffer.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to sign a message"); + + return signatureBuffer; +} + +QByteArray QOlmAccount::sign(const QJsonObject &message) const +{ + return sign(QJsonDocument(message).toJson(QJsonDocument::Compact)); +} + +QByteArray QOlmAccount::signIdentityKeys() const +{ + const auto keys = identityKeys(); + return sign(QJsonObject{ + { "algorithms", QJsonArray{ "m.olm.v1.curve25519-aes-sha2", + "m.megolm.v1.aes-sha2" } }, + { "user_id", m_userId }, + { "device_id", m_deviceId }, + { "keys", QJsonObject{ { QStringLiteral("curve25519:") + m_deviceId, + QString::fromUtf8(keys.curve25519) }, + { QStringLiteral("ed25519:") + m_deviceId, + QString::fromUtf8(keys.ed25519) } } } }); +} + +size_t QOlmAccount::maxNumberOfOneTimeKeys() const +{ + return olm_account_max_number_of_one_time_keys(m_account); +} + +size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) +{ + const auto randomLength = + olm_account_generate_one_time_keys_random_length(m_account, + numberOfKeys); + const auto result = olm_account_generate_one_time_keys( + m_account, numberOfKeys, RandomBuffer(randomLength), randomLength); + + if (result == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable( + "Failed to generate one-time keys for account " + accountId())); + + emit needsSave(); + return result; +} + +UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const +{ + const auto oneTimeKeyLength = olm_account_one_time_keys_length(m_account); + QByteArray oneTimeKeysBuffer(static_cast<int>(oneTimeKeyLength), '\0'); + + if (olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), + oneTimeKeyLength) + == olm_error()) + QOLM_INTERNAL_ERROR(qPrintable( + "Failed to obtain one-time keys for account" % accountId())); + + const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); + UnsignedOneTimeKeys oneTimeKeys; + fromJson(json, oneTimeKeys.keys); + return oneTimeKeys; +} + +OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const +{ + OneTimeKeys signedOneTimeKeys; + for (const auto& curveKeys = keys.curve25519(); + const auto& [keyId, key] : asKeyValueRange(curveKeys)) + signedOneTimeKeys.insert("signed_curve25519:" % keyId, + SignedOneTimeKey { + key, m_userId, m_deviceId, + sign(QJsonObject { { "key", key } }) }); + return signedOneTimeKeys; +} + +OlmErrorCode QOlmAccount::removeOneTimeKeys(const QOlmSession& session) +{ + if (olm_remove_one_time_keys(m_account, session.raw()) == olm_error()) { + qWarning(E2EE).nospace() + << "Failed to remove one-time keys for session " + << session.sessionId() << ": " << lastError(); + return lastErrorCode(); + } + emit needsSave(); + return OLM_SUCCESS; +} + +OlmAccount* QOlmAccount::data() { return m_account; } + +DeviceKeys QOlmAccount::deviceKeys() const +{ + static QStringList Algorithms(SupportedAlgorithms.cbegin(), + SupportedAlgorithms.cend()); + + const auto idKeys = identityKeys(); + return DeviceKeys{ + .userId = m_userId, + .deviceId = m_deviceId, + .algorithms = Algorithms, + .keys{ { "curve25519:" + m_deviceId, idKeys.curve25519 }, + { "ed25519:" + m_deviceId, idKeys.ed25519 } }, + .signatures{ + { m_userId, { { "ed25519:" + m_deviceId, signIdentityKeys() } } } } + }; +} + +UploadKeysJob* QOlmAccount::createUploadKeyRequest( + const UnsignedOneTimeKeys& oneTimeKeys) const +{ + return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys)); +} + +QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSession( + const QOlmMessage& preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSession(this, preKeyMessage); +} + +QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage) +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, + preKeyMessage); +} + +QOlmExpected<QOlmSessionPtr> QOlmAccount::createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey) +{ + return QOlmSession::createOutboundSession(this, theirIdentityKey, + theirOneTimeKey); +} + +void QOlmAccount::markKeysAsPublished() +{ + olm_account_mark_keys_as_published(m_account); + emit needsSave(); +} + +bool Quotient::verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId) +{ + const auto signKeyId = "ed25519:" + deviceId; + const auto signingKey = deviceKeys.keys[signKeyId]; + const auto signature = deviceKeys.signatures[userId][signKeyId]; + + return ed25519VerifySignature(signingKey, toJson(deviceKeys), signature); +} + +bool Quotient::ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature) +{ + if (signature.isEmpty()) + return false; + + QJsonObject obj1 = obj; + + obj1.remove("unsigned"); + obj1.remove("signatures"); + + auto canonicalJson = QJsonDocument(obj1).toJson(QJsonDocument::Compact); + + QByteArray signingKeyBuf = signingKey.toUtf8(); + QOlmUtility utility; + auto signatureBuf = signature.toUtf8(); + return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); +} + +QString QOlmAccount::accountId() const { return m_userId % '/' % m_deviceId; } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h new file mode 100644 index 00000000..a5faa82a --- /dev/null +++ b/lib/e2ee/qolmaccount.h @@ -0,0 +1,123 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + + +#pragma once + +#include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" + +#include "csapi/keys.h" + +#include <QtCore/QObject> + +struct OlmAccount; + +namespace Quotient { + +//! An olm account manages all cryptographic keys used on a device. +//! \code{.cpp} +//! const auto olmAccount = new QOlmAccount(this); +//! \endcode +class QUOTIENT_API QOlmAccount : public QObject +{ + Q_OBJECT +public: + QOlmAccount(QStringView userId, QStringView deviceId, + QObject* parent = nullptr); + ~QOlmAccount() override; + + //! Creates a new instance of OlmAccount. During the instantiation + //! the Ed25519 fingerprint key pair and the Curve25519 identity key + //! pair are generated. For more information see <a + //! href="https://matrix.org/docs/guides/e2e_implementation.html#keys-used-in-end-to-end-encryption">here</a>. + //! This needs to be called before any other action or use unpickle() instead. + void createNewAccount(); + + //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmAccount`. + //! This needs to be called before any other action or use createNewAccount() instead. + [[nodiscard]] OlmErrorCode unpickle(QByteArray&& pickled, + const PicklingMode& mode); + + //! Serialises an OlmAccount to encrypted Base64. + QByteArray pickle(const PicklingMode &mode); + + //! Returns the account's public identity keys already formatted as JSON + IdentityKeys identityKeys() const; + + //! Returns the signature of the supplied message. + QByteArray sign(const QByteArray &message) const; + QByteArray sign(const QJsonObject& message) const; + + //! Sign identity keys. + QByteArray signIdentityKeys() const; + + //! Maximum number of one time keys that this OlmAccount can + //! currently hold. + size_t maxNumberOfOneTimeKeys() const; + + //! Generates the supplied number of one time keys. + size_t generateOneTimeKeys(size_t numberOfKeys); + + //! Gets the OlmAccount's one time keys formatted as JSON. + UnsignedOneTimeKeys oneTimeKeys() const; + + //! Sign all one time keys. + OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const; + + UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const; + + DeviceKeys deviceKeys() const; + + //! Remove the one time key used to create the supplied session. + [[nodiscard]] OlmErrorCode removeOneTimeKeys(const QOlmSession& session); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param preKeyMessage An Olm pre-key message that was encrypted for this account. + QOlmExpected<QOlmSessionPtr> createInboundSession( + const QOlmMessage& preKeyMessage); + + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + //! + //! \param theirIdentityKey - The identity key of the Olm account that + //! encrypted this Olm message. + QOlmExpected<QOlmSessionPtr> createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage); + + //! Creates an outbound session for sending messages to a specific + /// identity and one time key. + QOlmExpected<QOlmSessionPtr> createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey); + + void markKeysAsPublished(); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + + // HACK do not use directly + QOlmAccount(OlmAccount *account); + OlmAccount *data(); + +Q_SIGNALS: + void needsSave(); + +private: + OlmAccount *m_account = nullptr; // owning + QString m_userId; + QString m_deviceId; + + QString accountId() const; +}; + +QUOTIENT_API bool verifyIdentitySignature(const DeviceKeys& deviceKeys, + const QString& deviceId, + const QString& userId); + +//! checks if the signature is signed by the signing_key +QUOTIENT_API bool ed25519VerifySignature(const QString& signingKey, + const QJsonObject& obj, + const QString& signature); + +} // namespace Quotient diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp new file mode 100644 index 00000000..18275dc0 --- /dev/null +++ b/lib/e2ee/qolminboundsession.cpp @@ -0,0 +1,192 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolminboundsession.h" +#include "qolmutils.h" +#include "../logging.h" + +#include <cstring> +#include <iostream> +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmInboundGroupSession::lastErrorCode() const { + return olm_inbound_group_session_last_error_code(m_groupSession); +} + +const char* QOlmInboundGroupSession::lastError() const +{ + return olm_inbound_group_session_last_error(m_groupSession); +} + +QOlmInboundGroupSession::QOlmInboundGroupSession(OlmInboundGroupSession *session) + : m_groupSession(session) +{} + +QOlmInboundGroupSession::~QOlmInboundGroupSession() +{ + olm_clear_inbound_group_session(m_groupSession); + //delete[](reinterpret_cast<uint8_t *>(m_groupSession)); +} + +QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::create( + const QByteArray& key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + if (olm_init_inbound_group_session( + olmInboundGroupSession, + reinterpret_cast<const uint8_t*>(key.constData()), key.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastErrorCode() + qWarning(E2EE) << "Failed to create an inbound group session:" + << olm_inbound_group_session_last_error( + olmInboundGroupSession); + return olm_inbound_group_session_last_error_code(olmInboundGroupSession); + } + + return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession); +} + +QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::importSession( + const QByteArray& key) +{ + const auto olmInboundGroupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + + if (olm_import_inbound_group_session( + olmInboundGroupSession, + reinterpret_cast<const uint8_t*>(key.data()), key.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastError() + qWarning(E2EE) << "Failed to import an inbound group session:" + << olm_inbound_group_session_last_error( + olmInboundGroupSession); + return olm_inbound_group_session_last_error_code(olmInboundGroupSession); + } + + return std::make_unique<QOlmInboundGroupSession>(olmInboundGroupSession); +} + +QByteArray QOlmInboundGroupSession::pickle(const PicklingMode& mode) const +{ + QByteArray pickledBuf( + olm_pickle_inbound_group_session_length(m_groupSession), '\0'); + if (const auto key = toKey(mode); + olm_pickle_inbound_group_session(m_groupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.length()) + == olm_error()) { + QOLM_INTERNAL_ERROR("Failed to pickle the inbound group session"); + } + return pickledBuf; +} + +QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::unpickle( + QByteArray&& pickled, const PicklingMode& mode) +{ + const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); + auto key = toKey(mode); + if (olm_unpickle_inbound_group_session(groupSession, key.data(), + key.length(), pickled.data(), + pickled.size()) + == olm_error()) { + // FIXME: create QOlmInboundGroupSession earlier and use lastError() + qWarning(E2EE) << "Failed to unpickle an inbound group session:" + << olm_inbound_group_session_last_error(groupSession); + return olm_inbound_group_session_last_error_code(groupSession); + } + key.clear(); + + return std::make_unique<QOlmInboundGroupSession>(groupSession); +} + +QOlmExpected<std::pair<QByteArray, uint32_t>> QOlmInboundGroupSession::decrypt( + const QByteArray& message) +{ + // This is for capturing the output of olm_group_decrypt + uint32_t messageIndex = 0; + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(message.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + QByteArray plaintextBuf(olm_group_decrypt_max_plaintext_length( + m_groupSession, + reinterpret_cast<uint8_t*>(messageBuf.data()), + messageBuf.length()), + '\0'); + + messageBuf = QByteArray(message.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextLen = olm_group_decrypt(m_groupSession, reinterpret_cast<uint8_t *>(messageBuf.data()), + messageBuf.length(), reinterpret_cast<uint8_t *>(plaintextBuf.data()), plaintextBuf.length(), &messageIndex); + if (plaintextLen == olm_error()) { + qWarning(E2EE) << "Failed to decrypt the message:" << lastError(); + return lastErrorCode(); + } + + QByteArray output(plaintextLen, '\0'); + std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); + + return std::make_pair(output, messageIndex); +} + +QOlmExpected<QByteArray> QOlmInboundGroupSession::exportSession( + uint32_t messageIndex) +{ + const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); + QByteArray keyBuf(keyLength, '\0'); + if (olm_export_inbound_group_session( + m_groupSession, reinterpret_cast<uint8_t*>(keyBuf.data()), + keyLength, messageIndex) + == olm_error()) { + QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to export the inbound group session"); + return lastErrorCode(); + } + return keyBuf; +} + +uint32_t QOlmInboundGroupSession::firstKnownIndex() const +{ + return olm_inbound_group_session_first_known_index(m_groupSession); +} + +QByteArray QOlmInboundGroupSession::sessionId() const +{ + QByteArray sessionIdBuf(olm_inbound_group_session_id_length(m_groupSession), + '\0'); + if (olm_inbound_group_session_id( + m_groupSession, reinterpret_cast<uint8_t*>(sessionIdBuf.data()), + sessionIdBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain the group session id"); + + return sessionIdBuf; +} + +bool QOlmInboundGroupSession::isVerified() const +{ + return olm_inbound_group_session_is_verified(m_groupSession) != 0; +} + +QString QOlmInboundGroupSession::olmSessionId() const +{ + return m_olmSessionId; +} +void QOlmInboundGroupSession::setOlmSessionId(const QString& newOlmSessionId) +{ + m_olmSessionId = newOlmSessionId; +} + +QString QOlmInboundGroupSession::senderId() const +{ + return m_senderId; +} +void QOlmInboundGroupSession::setSenderId(const QString& senderId) +{ + m_senderId = senderId; +} diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h new file mode 100644 index 00000000..b9710354 --- /dev/null +++ b/lib/e2ee/qolminboundsession.h @@ -0,0 +1,60 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" + +struct OlmInboundGroupSession; + +namespace Quotient { + +//! An in-bound group session is responsible for decrypting incoming +//! communication in a Megolm session. +class QUOTIENT_API QOlmInboundGroupSession +{ +public: + ~QOlmInboundGroupSession(); + //! Creates a new instance of `OlmInboundGroupSession`. + static QOlmExpected<QOlmInboundGroupSessionPtr> create(const QByteArray& key); + //! Import an inbound group session, from a previous export. + static QOlmExpected<QOlmInboundGroupSessionPtr> importSession(const QByteArray& key); + //! Serialises an `OlmInboundGroupSession` to encrypted Base64. + QByteArray pickle(const PicklingMode& mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by pickling + //! an `OlmInboundGroupSession`. + static QOlmExpected<QOlmInboundGroupSessionPtr> unpickle( + QByteArray&& pickled, const PicklingMode& mode); + //! Decrypts ciphertext received for this group session. + QOlmExpected<std::pair<QByteArray, uint32_t> > decrypt(const QByteArray& message); + //! Export the base64-encoded ratchet key for this session, at the given index, + //! in a format which can be used by import. + QOlmExpected<QByteArray> exportSession(uint32_t messageIndex); + //! Get the first message index we know how to decrypt. + uint32_t firstKnownIndex() const; + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + bool isVerified() const; + + //! The olm session that this session was received from. + //! Required to get the device this session is from. + QString olmSessionId() const; + void setOlmSessionId(const QString& newOlmSessionId); + + //! The sender of this session. + QString senderId() const; + void setSenderId(const QString& senderId); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + + QOlmInboundGroupSession(OlmInboundGroupSession* session); +private: + OlmInboundGroupSession* m_groupSession; + QString m_olmSessionId; + QString m_senderId; +}; + +using QOlmInboundGroupSessionPtr = std::unique_ptr<QOlmInboundGroupSession>; +} // namespace Quotient diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp new file mode 100644 index 00000000..b9cb8bd2 --- /dev/null +++ b/lib/e2ee/qolmmessage.cpp @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmmessage.h" + +#include "util.h" + +using namespace Quotient; + +QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) + : QByteArray(std::move(ciphertext)) + , m_messageType(type) +{ + Q_ASSERT_X(!isEmpty(), "olm message", "Ciphertext is empty"); +} + +QOlmMessage::Type QOlmMessage::type() const +{ + return m_messageType; +} + +QByteArray QOlmMessage::toCiphertext() const +{ + return SLICE(*this, QByteArray); +} + +QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) +{ + return QOlmMessage(ciphertext, QOlmMessage::General); +} diff --git a/lib/e2ee/qolmmessage.h b/lib/e2ee/qolmmessage.h new file mode 100644 index 00000000..ea73b3e3 --- /dev/null +++ b/lib/e2ee/qolmmessage.h @@ -0,0 +1,42 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "quotient_export.h" + +#include <QtCore/QByteArray> +#include <qobjectdefs.h> +#include <olm/olm.h> + +namespace Quotient { + +/*! \brief A wrapper around an olm encrypted message + * + * This class encapsulates a Matrix olm encrypted message, + * passed in either of 2 forms: a general message or a pre-key message. + * + * The class provides functions to get a type and the ciphertext. + */ +class QUOTIENT_API QOlmMessage : public QByteArray { + Q_GADGET +public: + enum Type { + PreKey = OLM_MESSAGE_TYPE_PRE_KEY, + General = OLM_MESSAGE_TYPE_MESSAGE, + }; + Q_ENUM(Type) + + explicit QOlmMessage(QByteArray ciphertext, Type type = General); + + static QOlmMessage fromCiphertext(const QByteArray &ciphertext); + + Q_INVOKABLE Type type() const; + Q_INVOKABLE QByteArray toCiphertext() const; + +private: + Type m_messageType = General; +}; + +} //namespace Quotient diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp new file mode 100644 index 00000000..1176d790 --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -0,0 +1,152 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmoutboundsession.h" + +#include "logging.h" +#include "qolmutils.h" + +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmOutboundGroupSession::lastErrorCode() const { + return olm_outbound_group_session_last_error_code(m_groupSession); +} + +const char* QOlmOutboundGroupSession::lastError() const +{ + return olm_outbound_group_session_last_error(m_groupSession); +} + +QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) + : m_groupSession(session) +{} + +QOlmOutboundGroupSession::~QOlmOutboundGroupSession() +{ + olm_clear_outbound_group_session(m_groupSession); + delete[](reinterpret_cast<uint8_t *>(m_groupSession)); +} + +QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() +{ + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + if (const auto randomLength = olm_init_outbound_group_session_random_length( + olmOutboundGroupSession); + olm_init_outbound_group_session(olmOutboundGroupSession, + RandomBuffer(randomLength).bytes(), + randomLength) + == olm_error()) { + // FIXME: create the session object earlier + QOLM_INTERNAL_ERROR_X("Failed to initialise an outbound group session", + olm_outbound_group_session_last_error( + olmOutboundGroupSession)); + } + + return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); +} + +QByteArray QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf( + olm_pickle_outbound_group_session_length(m_groupSession), '\0'); + auto key = toKey(mode); + if (olm_pickle_outbound_group_session(m_groupSession, key.data(), + key.length(), pickledBuf.data(), + pickledBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to pickle the outbound group session"); + + key.clear(); + return pickledBuf; +} + +QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle( + QByteArray&& pickled, const PicklingMode& mode) +{ + auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); + auto key = toKey(mode); + if (olm_unpickle_outbound_group_session(olmOutboundGroupSession, key.data(), + key.length(), pickled.data(), + pickled.length()) + == olm_error()) { + // FIXME: create the session object earlier and use lastError() + qWarning(E2EE) << "Failed to unpickle an outbound group session:" + << olm_outbound_group_session_last_error( + olmOutboundGroupSession); + return olm_outbound_group_session_last_error_code( + olmOutboundGroupSession); + } + + key.clear(); + return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); +} + +QByteArray QOlmOutboundGroupSession::encrypt(const QByteArray& plaintext) const +{ + const auto messageMaxLength = + olm_group_encrypt_message_length(m_groupSession, plaintext.length()); + QByteArray messageBuf(messageMaxLength, '\0'); + if (olm_group_encrypt(m_groupSession, + reinterpret_cast<const uint8_t*>(plaintext.data()), + plaintext.length(), + reinterpret_cast<uint8_t*>(messageBuf.data()), + messageBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to encrypt a message"); + + return messageBuf; +} + +uint32_t QOlmOutboundGroupSession::sessionMessageIndex() const +{ + return olm_outbound_group_session_message_index(m_groupSession); +} + +QByteArray QOlmOutboundGroupSession::sessionId() const +{ + const auto idMaxLength = olm_outbound_group_session_id_length(m_groupSession); + QByteArray idBuffer(idMaxLength, '\0'); + if (olm_outbound_group_session_id( + m_groupSession, reinterpret_cast<uint8_t*>(idBuffer.data()), + idBuffer.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain group session id"); + + return idBuffer; +} + +QByteArray QOlmOutboundGroupSession::sessionKey() const +{ + const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); + QByteArray keyBuffer(keyMaxLength, '\0'); + if (olm_outbound_group_session_key( + m_groupSession, reinterpret_cast<uint8_t*>(keyBuffer.data()), + keyMaxLength) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain group session key"); + + return keyBuffer; +} + +int QOlmOutboundGroupSession::messageCount() const +{ + return m_messageCount; +} + +void QOlmOutboundGroupSession::setMessageCount(int messageCount) +{ + m_messageCount = messageCount; +} + +QDateTime QOlmOutboundGroupSession::creationTime() const +{ + return m_creationTime; +} + +void QOlmOutboundGroupSession::setCreationTime(const QDateTime& creationTime) +{ + m_creationTime = creationTime; +} diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h new file mode 100644 index 00000000..d36fbf69 --- /dev/null +++ b/lib/e2ee/qolmoutboundsession.h @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" + +struct OlmOutboundGroupSession; + +namespace Quotient { + +//! An out-bound group session is responsible for encrypting outgoing +//! communication in a Megolm session. +class QUOTIENT_API QOlmOutboundGroupSession +{ +public: + ~QOlmOutboundGroupSession(); + //! Creates a new instance of `QOlmOutboundGroupSession`. + //! Throw OlmError on errors + static QOlmOutboundGroupSessionPtr create(); + //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. + QByteArray pickle(const PicklingMode &mode) const; + //! Deserialises from encrypted Base64 that was previously obtained by + //! pickling a `QOlmOutboundGroupSession`. + static QOlmExpected<QOlmOutboundGroupSessionPtr> unpickle( + QByteArray&& pickled, const PicklingMode& mode); + + //! Encrypts a plaintext message using the session. + QByteArray encrypt(const QByteArray& plaintext) const; + + //! Get the current message index for this session. + //! + //! Each message is sent with an increasing index; this returns the + //! index for the next message. + uint32_t sessionMessageIndex() const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! Get the base64-encoded current ratchet key for this session. + //! + //! Each message is sent with a different ratchet key. This function returns the + //! ratchet key that will be used for the next message. + QByteArray sessionKey() const; + QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); + + int messageCount() const; + void setMessageCount(int messageCount); + + QDateTime creationTime() const; + void setCreationTime(const QDateTime& creationTime); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + +private: + OlmOutboundGroupSession *m_groupSession; + int m_messageCount = 0; + QDateTime m_creationTime = QDateTime::currentDateTime(); +}; + +} // namespace Quotient diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp new file mode 100644 index 00000000..e3f69132 --- /dev/null +++ b/lib/e2ee/qolmsession.cpp @@ -0,0 +1,231 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "qolmsession.h" + +#include "e2ee/qolmutils.h" +#include "logging.h" + +#include <cstring> +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmSession::lastErrorCode() const { + return olm_session_last_error_code(m_session); +} + +const char* QOlmSession::lastError() const +{ + return olm_session_last_error(m_session); +} + +Quotient::QOlmSession::~QOlmSession() +{ + olm_clear_session(m_session); + delete[](reinterpret_cast<uint8_t *>(m_session)); +} + +OlmSession* QOlmSession::create() +{ + return olm_session(new uint8_t[olm_session_size()]); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createInbound( + QOlmAccount* account, const QOlmMessage& preKeyMessage, bool from, + const QString& theirIdentityKey) +{ + if (preKeyMessage.type() != QOlmMessage::PreKey) { + qCCritical(E2EE) << "The message is not a pre-key; will try to create " + "the inbound session anyway"; + } + + const auto olmSession = create(); + + QByteArray oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + QByteArray theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + size_t error = 0; + if (from) { + error = olm_create_inbound_session_from(olmSession, account->data(), theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } else { + error = olm_create_inbound_session(olmSession, account->data(), oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + } + + if (error == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + const auto lastErr = olm_session_last_error_code(olmSession); + qCWarning(E2EE) << "Error when creating inbound session" << lastErr; + return lastErr; + } + + return std::make_unique<QOlmSession>(olmSession); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage) +{ + return createInbound(account, preKeyMessage); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage) +{ + return createInbound(account, preKeyMessage, true, theirIdentityKey); +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::createOutboundSession( + QOlmAccount* account, const QByteArray& theirIdentityKey, + const QByteArray& theirOneTimeKey) +{ + auto* olmOutboundSession = create(); + if (const auto randomLength = + olm_create_outbound_session_random_length(olmOutboundSession); + olm_create_outbound_session( + olmOutboundSession, account->data(), theirIdentityKey.data(), + theirIdentityKey.length(), theirOneTimeKey.data(), + theirOneTimeKey.length(), RandomBuffer(randomLength), randomLength) + == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + const auto lastErr = olm_session_last_error_code(olmOutboundSession); + QOLM_FAIL_OR_LOG_X(lastErr == OLM_NOT_ENOUGH_RANDOM, + "Failed to create an outbound Olm session", + olm_session_last_error(olmOutboundSession)); + return lastErr; + } + + return std::make_unique<QOlmSession>(olmOutboundSession); +} + +QByteArray QOlmSession::pickle(const PicklingMode &mode) const +{ + QByteArray pickledBuf(olm_pickle_session_length(m_session), '\0'); + QByteArray key = toKey(mode); + if (olm_pickle_session(m_session, key.data(), key.length(), + pickledBuf.data(), pickledBuf.length()) + == olm_error()) + QOLM_INTERNAL_ERROR("Failed to pickle an Olm session"); + + key.clear(); + return pickledBuf; +} + +QOlmExpected<QOlmSessionPtr> QOlmSession::unpickle(QByteArray&& pickled, + const PicklingMode& mode) +{ + auto *olmSession = create(); + auto key = toKey(mode); + if (olm_unpickle_session(olmSession, key.data(), key.length(), + pickled.data(), pickled.length()) + == olm_error()) { + // FIXME: the QOlmSession object should be created earlier + const auto errorCode = olm_session_last_error_code(olmSession); + QOLM_FAIL_OR_LOG_X(errorCode == OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to unpickle an Olm session", + olm_session_last_error(olmSession)); + return errorCode; + } + + key.clear(); + return std::make_unique<QOlmSession>(olmSession); +} + +QOlmMessage QOlmSession::encrypt(const QByteArray& plaintext) +{ + const auto messageMaxLength = + olm_encrypt_message_length(m_session, plaintext.length()); + QByteArray messageBuf(messageMaxLength, '\0'); + // NB: The type has to be calculated before calling olm_encrypt() + const auto messageType = olm_encrypt_message_type(m_session); + if (const auto randomLength = olm_encrypt_random_length(m_session); + olm_encrypt(m_session, plaintext.data(), plaintext.length(), + RandomBuffer(randomLength), randomLength, messageBuf.data(), + messageBuf.length()) + == olm_error()) { + QOLM_INTERNAL_ERROR("Failed to encrypt the message"); + } + + return QOlmMessage(messageBuf, QOlmMessage::Type(messageType)); +} + +QOlmExpected<QByteArray> QOlmSession::decrypt(const QOlmMessage &message) const +{ + const auto ciphertext = message.toCiphertext(); + const auto messageTypeValue = message.type(); + + // We need to clone the message because + // olm_decrypt_max_plaintext_length destroys the input buffer + QByteArray messageBuf(ciphertext.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf.begin()); + + const auto plaintextMaxLen = olm_decrypt_max_plaintext_length( + m_session, messageTypeValue, messageBuf.data(), messageBuf.length()); + if (plaintextMaxLen == olm_error()) { + qWarning(E2EE) << "Couldn't calculate decrypted message length:" + << lastError(); + return lastErrorCode(); + } + + QByteArray plaintextBuf(plaintextMaxLen, '\0'); + QByteArray messageBuf2(ciphertext.length(), '\0'); + std::copy(message.begin(), message.end(), messageBuf2.begin()); + + const auto plaintextResultLen = + olm_decrypt(m_session, messageTypeValue, messageBuf2.data(), + messageBuf2.length(), plaintextBuf.data(), plaintextMaxLen); + if (plaintextResultLen == olm_error()) { + QOLM_FAIL_OR_LOG(OLM_OUTPUT_BUFFER_TOO_SMALL, + "Failed to decrypt the message"); + return lastErrorCode(); + } + plaintextBuf.truncate(plaintextResultLen); + return plaintextBuf; +} + +QByteArray QOlmSession::sessionId() const +{ + const auto idMaxLength = olm_session_id_length(m_session); + QByteArray idBuffer(idMaxLength, '\0'); + if (olm_session_id(m_session, idBuffer.data(), idMaxLength) == olm_error()) + QOLM_INTERNAL_ERROR("Failed to obtain Olm session id"); + + return idBuffer; +} + +bool QOlmSession::hasReceivedMessage() const +{ + return olm_session_has_received_message(m_session); +} + +bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const +{ + Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); + QByteArray oneTimeKeyBuf(preKeyMessage.data()); + const auto maybeMatches = + olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), + oneTimeKeyBuf.length()); + if (maybeMatches == olm_error()) + qWarning(E2EE) << "Error matching an inbound session:" << lastError(); + + return maybeMatches == 1; // Any errors are treated as non-match +} + +bool QOlmSession::matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const +{ + const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); + auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); + const auto maybeMatches = olm_matches_inbound_session_from( + m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + + if (maybeMatches == olm_error()) + qCWarning(E2EE) << "Error matching an inbound session:" << lastError(); + + return maybeMatches == 1; +} + +QOlmSession::QOlmSession(OlmSession *session) + : m_session(session) +{} diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h new file mode 100644 index 00000000..400fb854 --- /dev/null +++ b/lib/e2ee/qolmsession.h @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: 2021 Alexey Andreyev <aa13q@ya.ru> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" +#include "e2ee/qolmmessage.h" +#include "e2ee/qolmaccount.h" + +struct OlmSession; + +namespace Quotient { + +//! Either an outbound or inbound session for secure communication. +class QUOTIENT_API QOlmSession +{ +public: + ~QOlmSession(); + //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. + static QOlmExpected<QOlmSessionPtr> createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage); + + static QOlmExpected<QOlmSessionPtr> createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage); + + static QOlmExpected<QOlmSessionPtr> createOutboundSession( + QOlmAccount* account, const QByteArray& theirIdentityKey, + const QByteArray& theirOneTimeKey); + + //! Serialises an `QOlmSession` to encrypted Base64. + QByteArray pickle(const PicklingMode &mode) const; + + //! Deserialises from encrypted Base64 previously made with pickle() + static QOlmExpected<QOlmSessionPtr> unpickle(QByteArray&& pickled, + const PicklingMode& mode); + + //! Encrypts a plaintext message using the session. + QOlmMessage encrypt(const QByteArray& plaintext); + + //! Decrypts a message using this session. Decoding is lossy, meaning if + //! the decrypted plaintext contains invalid UTF-8 symbols, they will + //! be returned as `U+FFFD` (�). + QOlmExpected<QByteArray> decrypt(const QOlmMessage &message) const; + + //! Get a base64-encoded identifier for this session. + QByteArray sessionId() const; + + //! Checker for any received messages for this session. + bool hasReceivedMessage() const; + + //! Checks if the 'prekey' message is for this in-bound session. + bool matchesInboundSession(const QOlmMessage& preKeyMessage) const; + + //! Checks if the 'prekey' message is for this in-bound session. + bool matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const; + + friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) + { + return lhs.sessionId() < rhs.sessionId(); + } + + friend bool operator<(const QOlmSessionPtr& lhs, const QOlmSessionPtr& rhs) + { + return *lhs < *rhs; + } + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + + OlmSession* raw() const { return m_session; } + + QOlmSession(OlmSession* session); +private: + //! Helper function for creating new sessions and handling errors. + static OlmSession* create(); + static QOlmExpected<QOlmSessionPtr> createInbound( + QOlmAccount* account, const QOlmMessage& preKeyMessage, + bool from = false, const QString& theirIdentityKey = ""); + OlmSession* m_session; +}; +} //namespace Quotient diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp new file mode 100644 index 00000000..46f7f4f3 --- /dev/null +++ b/lib/e2ee/qolmutility.cpp @@ -0,0 +1,53 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutility.h" + +#include <olm/olm.h> + +using namespace Quotient; + +OlmErrorCode QOlmUtility::lastErrorCode() const { + return olm_utility_last_error_code(m_utility); +} + +const char* QOlmUtility::lastError() const +{ + return olm_utility_last_error(m_utility); +} + +QOlmUtility::QOlmUtility() +{ + auto utility = new uint8_t[olm_utility_size()]; + m_utility = olm_utility(utility); +} + +QOlmUtility::~QOlmUtility() +{ + olm_clear_utility(m_utility); + delete[](reinterpret_cast<uint8_t *>(m_utility)); +} + +QString QOlmUtility::sha256Bytes(const QByteArray &inputBuf) const +{ + const auto outputLen = olm_sha256_length(m_utility); + QByteArray outputBuf(outputLen, '\0'); + olm_sha256(m_utility, inputBuf.data(), inputBuf.length(), + outputBuf.data(), outputBuf.length()); + + return QString::fromUtf8(outputBuf); +} + +QString QOlmUtility::sha256Utf8Msg(const QString &message) const +{ + return sha256Bytes(message.toUtf8()); +} + +bool QOlmUtility::ed25519Verify(const QByteArray& key, const QByteArray& message, + QByteArray signature) +{ + return olm_ed25519_verify(m_utility, key.data(), key.size(), message.data(), + message.size(), signature.data(), signature.size()) + == 0; +} diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h new file mode 100644 index 00000000..508767bf --- /dev/null +++ b/lib/e2ee/qolmutility.h @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "e2ee/e2ee.h" + +struct OlmUtility; + +namespace Quotient { + +//! Allows you to make use of crytographic hashing via SHA-2 and +//! verifying ed25519 signatures. +class QUOTIENT_API QOlmUtility +{ +public: + QOlmUtility(); + ~QOlmUtility(); + + //! Returns a sha256 of the supplied byte slice. + QString sha256Bytes(const QByteArray &inputBuf) const; + + //! Convenience function that converts the UTF-8 message + //! to bytes and then calls `sha256Bytes()`, returning its output. + QString sha256Utf8Msg(const QString &message) const; + + //! Verify a ed25519 signature. + //! \param key QByteArray The public part of the ed25519 key that signed the message. + //! \param message QByteArray The message that was signed. + //! \param signature QByteArray The signature of the message. + bool ed25519Verify(const QByteArray &key, + const QByteArray &message, QByteArray signature); + + OlmErrorCode lastErrorCode() const; + const char* lastError() const; + +private: + OlmUtility *m_utility; +}; +} diff --git a/lib/e2ee/qolmutils.cpp b/lib/e2ee/qolmutils.cpp new file mode 100644 index 00000000..c6e51bcd --- /dev/null +++ b/lib/e2ee/qolmutils.cpp @@ -0,0 +1,22 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "e2ee/qolmutils.h" +#include <QtCore/QRandomGenerator> + +using namespace Quotient; + +QByteArray Quotient::toKey(const Quotient::PicklingMode &mode) +{ + if (std::holds_alternative<Quotient::Unencrypted>(mode)) { + return {}; + } + return std::get<Quotient::Encrypted>(mode).key; +} + +RandomBuffer::RandomBuffer(size_t size) + : QByteArray(static_cast<int>(size), '\0') +{ + QRandomGenerator::system()->generate(begin(), end()); +} diff --git a/lib/e2ee/qolmutils.h b/lib/e2ee/qolmutils.h new file mode 100644 index 00000000..17eee7a3 --- /dev/null +++ b/lib/e2ee/qolmutils.h @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QByteArray> + +#include "e2ee/e2ee.h" + +namespace Quotient { + +// Convert PicklingMode to key +QUOTIENT_API QByteArray toKey(const PicklingMode &mode); + +class QUOTIENT_API RandomBuffer : public QByteArray { +public: + explicit RandomBuffer(size_t size); + ~RandomBuffer() { clear(); } + + // NOLINTNEXTLINE(google-explicit-constructor) + QUO_IMPLICIT operator void*() { return data(); } + char* chars() { return data(); } + uint8_t* bytes() { return reinterpret_cast<uint8_t*>(data()); } + + Q_DISABLE_COPY(RandomBuffer) + RandomBuffer(RandomBuffer&&) = default; + void operator=(RandomBuffer&&) = delete; +}; + +[[deprecated("Create RandomBuffer directly")]] inline auto getRandom( + size_t bufferSize) +{ + return RandomBuffer(bufferSize); +} + +#define QOLM_INTERNAL_ERROR_X(Message_, LastError_) \ + qFatal("%s, internal error: %s", Message_, LastError_) + +#define QOLM_INTERNAL_ERROR(Message_) \ + QOLM_INTERNAL_ERROR_X(Message_, lastError()) + +#define QOLM_FAIL_OR_LOG_X(InternalCondition_, Message_, LastErrorText_) \ + do { \ + const QString errorMsg{ (Message_) }; \ + if (InternalCondition_) \ + QOLM_INTERNAL_ERROR_X(qPrintable(errorMsg), (LastErrorText_)); \ + qWarning(E2EE).nospace() << errorMsg << ": " << (LastErrorText_); \ + } while (false) /* End of macro */ + +#define QOLM_FAIL_OR_LOG(InternalFailureValue_, Message_) \ + QOLM_FAIL_OR_LOG_X(lastErrorCode() == (InternalFailureValue_), (Message_), \ + lastError()) + +} // namespace Quotient diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp deleted file mode 100644 index 4a1025b2..00000000 --- a/lib/encryptionmanager.cpp +++ /dev/null @@ -1,369 +0,0 @@ -#ifdef Quotient_E2EE_ENABLED -#include "encryptionmanager.h" - -#include "connection.h" -#include "e2ee.h" - -#include "csapi/keys.h" - -#include <QtCore/QHash> -#include <QtCore/QStringBuilder> - -#include <account.h> // QtOlm -#include <session.h> // QtOlm -#include <message.h> // QtOlm -#include <errors.h> // QtOlm -#include <utils.h> // QtOlm -#include <functional> -#include <memory> - -using namespace Quotient; -using namespace QtOlm; -using std::move; - -class EncryptionManager::Private { -public: - explicit Private(const QByteArray& encryptionAccountPickle, - float signedKeysProportion, float oneTimeKeyThreshold) - : q(nullptr) - , signedKeysProportion(move(signedKeysProportion)) - , oneTimeKeyThreshold(move(oneTimeKeyThreshold)) - { - Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); - Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); - if (encryptionAccountPickle.isEmpty()) { - olmAccount.reset(new Account()); - } else { - olmAccount.reset( - new Account(encryptionAccountPickle)); // TODO: passphrase even - // with qtkeychain? - } - /* - * Note about targetKeysNumber: - * - * From: https://github.com/Zil0/matrix-python-sdk/ - * File: matrix_client/crypto/olm_device.py - * - * Try to maintain half the number of one-time keys libolm can hold - * uploaded on the HS. This is because some keys will be claimed by - * peers but not used instantly, and we want them to stay in libolm, - * until the limit is reached and it starts discarding keys, starting by - * the oldest. - */ - targetKeysNumber = olmAccount->maxOneTimeKeys() / 2; - targetOneTimeKeyCounts = { - { SignedCurve25519Key, - qRound(signedKeysProportion * targetKeysNumber) }, - { Curve25519Key, - qRound((1 - signedKeysProportion) * targetKeysNumber) } - }; - updateKeysToUpload(); - } - ~Private() = default; - - EncryptionManager* q; - - UploadKeysJob* uploadIdentityKeysJob = nullptr; - UploadKeysJob* uploadOneTimeKeysInitJob = nullptr; - UploadKeysJob* uploadOneTimeKeysJob = nullptr; - QueryKeysJob* queryKeysJob = nullptr; - - QScopedPointer<Account> olmAccount; - - float signedKeysProportion; - float oneTimeKeyThreshold; - int targetKeysNumber; - - void updateKeysToUpload(); - bool oneTimeKeyShouldUpload(); - - QHash<QString, int> oneTimeKeyCounts; - void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue) - { - oneTimeKeyCounts = oneTimeKeyCountsNewValue; - updateKeysToUpload(); - } - QHash<QString, int> oneTimeKeysToUploadCounts; - QHash<QString, int> targetOneTimeKeyCounts; - - // A map from senderKey to InboundSession - QMap<QString, InboundSession*> sessions; // TODO: cache - void updateDeviceKeys( - const QHash<QString, - QHash<QString, QueryKeysJob::DeviceInformation>>& deviceKeys) - { - for (auto userId : deviceKeys.keys()) { - for (auto deviceId : deviceKeys.value(userId).keys()) { - auto info = deviceKeys.value(userId).value(deviceId); - // TODO: ed25519Verify, etc - } - } - } - QString sessionDecrypt(Message* message, const QString& senderKey) - { - QString decrypted; - QList<InboundSession*> senderSessions = sessions.values(senderKey); - // Try to decrypt message body using one of the known sessions for that - // device - bool sessionsPassed = false; - for (auto senderSession : senderSessions) { - if (senderSession == senderSessions.last()) { - sessionsPassed = true; - } - try { - decrypted = senderSession->decrypt(message); - qCDebug(E2EE) - << "Success decrypting Olm event using existing session" - << senderSession->id(); - break; - } catch (OlmError* e) { - if (message->messageType() == 0) { - PreKeyMessage preKeyMessage = - PreKeyMessage(message->cipherText()); - if (senderSession->matches(&preKeyMessage, senderKey)) { - // We had a matching session for a pre-key message, but - // it didn't work. This means something is wrong, so we - // fail now. - qCDebug(E2EE) - << "Error decrypting pre-key message with existing " - "Olm session" - << senderSession->id() << "reason:" << e->what(); - return QString(); - } - } - // Simply keep trying otherwise - } - } - if (sessionsPassed || senderSessions.empty()) { - if (message->messageType() > 0) { - // Not a pre-key message, we should have had a matching session - if (!sessions.empty()) { - qCDebug(E2EE) << "Error decrypting with existing sessions"; - return QString(); - } - qCDebug(E2EE) << "No existing sessions"; - return QString(); - } - // We have a pre-key message without any matching session, in this - // case we should try to create one. - InboundSession* newSession; - qCDebug(E2EE) << "try to establish new InboundSession with" << senderKey; - PreKeyMessage preKeyMessage = PreKeyMessage(message->cipherText()); - try { - newSession = new InboundSession(olmAccount.data(), - &preKeyMessage, - senderKey.toLatin1(), q); - } catch (OlmError* e) { - qCDebug(E2EE) << "Error decrypting pre-key message when trying " - "to establish a new session:" - << e->what(); - return QString(); - } - qCDebug(E2EE) << "Created new Olm session" << newSession->id(); - try { - decrypted = newSession->decrypt(message); - } catch (OlmError* e) { - qCDebug(E2EE) - << "Error decrypting pre-key message with new session" - << e->what(); - return QString(); - } - olmAccount->removeOneTimeKeys(newSession); - sessions.insert(senderKey, newSession); - } - return decrypted; - } -}; - -EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, - float signedKeysProportion, - float oneTimeKeyThreshold, QObject* parent) - : QObject(parent) - , d(std::make_unique<Private>(std::move(encryptionAccountPickle), - std::move(signedKeysProportion), - std::move(oneTimeKeyThreshold))) -{ - d->q = this; -} - -EncryptionManager::~EncryptionManager() = default; - -void EncryptionManager::uploadIdentityKeys(Connection* connection) -{ - // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload - DeviceKeys deviceKeys { - /* - * The ID of the user the device belongs to. Must match the user ID used - * when logging in. The ID of the device these keys belong to. Must - * match the device ID used when logging in. The encryption algorithms - * supported by this device. - */ - connection->userId(), - connection->deviceId(), - SupportedAlgorithms, - /* - * Public identity keys. The names of the properties should be in the - * format <algorithm>:<device_id>. The keys themselves should be encoded - * as specified by the key algorithm. - */ - { { Curve25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->curve25519IdentityKey() }, - { Ed25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->ed25519IdentityKey() } }, - /* signatures should be provided after the unsigned deviceKeys - generation */ - {} - }; - - QJsonObject deviceKeysJsonObject = toJson(deviceKeys); - /* additionally removing signatures key, - * since we could not initialize deviceKeys - * without an empty signatures value: - */ - deviceKeysJsonObject.remove(QStringLiteral("signatures")); - /* - * Signatures for the device key object. - * A map from user ID, to a map from <algorithm>:<device_id> to the - * signature. The signature is calculated using the process called Signing - * JSON. - */ - deviceKeys.signatures = { - { connection->userId(), - { { Ed25519Key + QStringLiteral(":") + connection->deviceId(), - d->olmAccount->sign(deviceKeysJsonObject) } } } - }; - - d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys); - connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); - }); -} - -void EncryptionManager::uploadOneTimeKeys(Connection* connection, - bool forceUpdate) -{ - if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { - d->uploadOneTimeKeysInitJob = connection->callApi<UploadKeysJob>(); - connect(d->uploadOneTimeKeysInitJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); - }); - } - - int signedKeysToUploadCount = - d->oneTimeKeysToUploadCounts.value(SignedCurve25519Key, 0); - int unsignedKeysToUploadCount = - d->oneTimeKeysToUploadCounts.value(Curve25519Key, 0); - - d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount - + unsignedKeysToUploadCount); - - QHash<QString, QVariant> oneTimeKeys = {}; - const auto& olmAccountCurve25519OneTimeKeys = - d->olmAccount->curve25519OneTimeKeys(); - - int oneTimeKeysCounter = 0; - for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); - it != olmAccountCurve25519OneTimeKeys.cend(); ++it) { - QString keyId = it.key(); - QString keyType; - QVariant key; - if (oneTimeKeysCounter < signedKeysToUploadCount) { - QJsonObject message { { QStringLiteral("key"), - it.value().toString() } }; - - QByteArray signedMessage = d->olmAccount->sign(message); - QJsonObject signatures { - { connection->userId(), - QJsonObject { { Ed25519Key + QStringLiteral(":") - + connection->deviceId(), - QString::fromUtf8(signedMessage) } } } - }; - message.insert(QStringLiteral("signatures"), signatures); - key = message; - keyType = SignedCurve25519Key; - } else { - key = it.value(); - keyType = Curve25519Key; - } - ++oneTimeKeysCounter; - oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); - } - d->uploadOneTimeKeysJob = - connection->callApi<UploadKeysJob>(none, oneTimeKeys); - connect(d->uploadOneTimeKeysJob, &BaseJob::success, this, [this] { - d->setOneTimeKeyCounts(d->uploadOneTimeKeysJob->oneTimeKeyCounts()); - }); - d->olmAccount->markKeysAsPublished(); - qCDebug(E2EE) << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") - .arg(signedKeysToUploadCount) - .arg(unsignedKeysToUploadCount); -} - -void EncryptionManager::updateOneTimeKeyCounts( - Connection* connection, const QHash<QString, int>& deviceOneTimeKeysCount) -{ - d->oneTimeKeyCounts = deviceOneTimeKeysCount; - if (d->oneTimeKeyShouldUpload()) { - qCDebug(E2EE) << "Uploading new one-time keys."; - uploadOneTimeKeys(connection); - } -} - -void Quotient::EncryptionManager::updateDeviceKeys( - Connection* connection, const QHash<QString, QStringList>& deviceKeys) -{ - d->queryKeysJob = connection->callApi<QueryKeysJob>(deviceKeys); - connect(d->queryKeysJob, &BaseJob::success, this, - [this] { d->updateDeviceKeys(d->queryKeysJob->deviceKeys()); }); -} - -QString EncryptionManager::sessionDecryptMessage( - const QJsonObject& personalCipherObject, const QByteArray& senderKey) -{ - QString decrypted; - int type = personalCipherObject.value(TypeKeyL).toInt(-1); - QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); - if (type == 0) { - PreKeyMessage preKeyMessage { body }; - decrypted = d->sessionDecrypt(reinterpret_cast<Message*>(&preKeyMessage), - senderKey); - } else if (type == 1) { - Message message { body }; - decrypted = d->sessionDecrypt(&message, senderKey); - } - return decrypted; -} - -QByteArray EncryptionManager::olmAccountPickle() -{ - return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? -} - -QtOlm::Account* EncryptionManager::account() const -{ - return d->olmAccount.data(); -} - -void EncryptionManager::Private::updateKeysToUpload() -{ - for (auto it = targetOneTimeKeyCounts.cbegin(); - it != targetOneTimeKeyCounts.cend(); ++it) { - int numKeys = oneTimeKeyCounts.value(it.key(), 0); - int numToCreate = qMax(it.value() - numKeys, 0); - oneTimeKeysToUploadCounts.insert(it.key(), numToCreate); - } -} - -bool EncryptionManager::Private::oneTimeKeyShouldUpload() -{ - if (oneTimeKeyCounts.empty()) - return true; - for (auto it = targetOneTimeKeyCounts.cbegin(); - it != targetOneTimeKeyCounts.cend(); ++it) { - if (oneTimeKeyCounts.value(it.key(), 0) - < it.value() * oneTimeKeyThreshold) - return true; - } - return false; -} -#endif // Quotient_E2EE_ENABLED diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h deleted file mode 100644 index 5df15e83..00000000 --- a/lib/encryptionmanager.h +++ /dev/null @@ -1,47 +0,0 @@ -#ifdef Quotient_E2EE_ENABLED -#pragma once - -#include <QtCore/QObject> - -#include <functional> -#include <memory> - -namespace QtOlm { -class Account; -} - -namespace Quotient { -class Connection; - -class EncryptionManager : public QObject { - Q_OBJECT - -public: - // TODO: store constats separately? - // TODO: 0.5 oneTimeKeyThreshold instead of 0.1? - explicit EncryptionManager( - const QByteArray& encryptionAccountPickle = QByteArray(), - float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1), - QObject* parent = nullptr); - ~EncryptionManager(); - - void uploadIdentityKeys(Connection* connection); - void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); - void - updateOneTimeKeyCounts(Connection* connection, - const QHash<QString, int>& deviceOneTimeKeysCount); - void updateDeviceKeys(Connection* connection, - const QHash<QString, QStringList>& deviceKeys); - QString sessionDecryptMessage(const QJsonObject& personalCipherObject, - const QByteArray& senderKey); - QByteArray olmAccountPickle(); - - QtOlm::Account* account() const; - -private: - class Private; - std::unique_ptr<Private> d; -}; - -} // namespace Quotient -#endif // Quotient_E2EE_ENABLED diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 2e2b11c0..a2e2a156 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "eventitem.h" @@ -23,20 +8,25 @@ using namespace Quotient; -void PendingEventItem::setFileUploaded(const QUrl& remoteUrl) +void PendingEventItem::setFileUploaded(const FileSourceInfo& uploadedFileData) { // TODO: eventually we might introduce hasFileContent to RoomEvent, // and unify the code below. if (auto* rme = getAs<RoomMessageEvent>()) { Q_ASSERT(rme->hasFileContent()); - rme->editContent([remoteUrl](EventContent::TypedBase& ec) { - ec.fileInfo()->url = remoteUrl; + rme->editContent([&uploadedFileData](EventContent::TypedBase& ec) { + ec.fileInfo()->source = uploadedFileData; }); } if (auto* rae = getAs<RoomAvatarEvent>()) { Q_ASSERT(rae->content().fileInfo()); - rae->editContent( - [remoteUrl](EventContent::FileInfo& fi) { fi.url = remoteUrl; }); + rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) { + fi.source = uploadedFileData; + }); } setStatus(EventStatus::FileUploaded); } + +// Not exactly sure why but this helps with the linker not finding +// Quotient::EventStatus::staticMetaObject when building Quaternion +#include "moc_eventitem.cpp" diff --git a/lib/eventitem.h b/lib/eventitem.h index 7b2c3c44..96e45b38 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -1,55 +1,43 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "quotient_common.h" + +#include "events/callevents.h" +#include "events/filesourceinfo.h" #include "events/stateevent.h" +#include <any> #include <utility> namespace Quotient { -class StateEventBase; -class EventStatus { - Q_GADGET -public: +namespace EventStatus { + Q_NAMESPACE_EXPORT(QUOTIENT_API) + /** Special marks an event can assume * * This is used to hint at a special status of some events in UI. * All values except Redacted and Hidden are mutually exclusive. */ enum Code { - Normal = 0x0, //< No special designation - Submitted = 0x01, //< The event has just been submitted for sending - FileUploaded = 0x02, //< The file attached to the event has been - // uploaded to the server - Departed = 0x03, //< The event has left the client - ReachedServer = 0x04, //< The server has received the event - SendingFailed = 0x05, //< The server could not receive the event - Redacted = 0x08, //< The event has been redacted - Replaced = 0x10, //< The event has been replaced - Hidden = 0x100, //< The event should not be shown in the timeline + Normal = 0x0, ///< No special designation + Submitted = 0x01, ///< The event has just been submitted for sending + FileUploaded = 0x02, ///< The file attached to the event has been + /// uploaded to the server + Departed = 0x03, ///< The event has left the client + ReachedServer = 0x04, ///< The server has received the event + SendingFailed = 0x05, ///< The server could not receive the event + Redacted = 0x08, ///< The event has been redacted + Replaced = 0x10, ///< The event has been replaced + Hidden = 0x100, ///< The event should not be shown in the timeline }; - Q_DECLARE_FLAGS(Status, Code) - Q_FLAG(Status) -}; + Q_ENUM_NS(Code) +} // namespace EventStatus -class EventItemBase { +class QUOTIENT_API EventItemBase { public: explicit EventItemBase(RoomEventPtr&& e) : evt(std::move(e)) { @@ -58,7 +46,7 @@ public: const RoomEvent* event() const { return rawPtr(evt); } const RoomEvent* get() const { return event(); } - template <typename EventT> + template <EventClass<RoomEvent> EventT> const EventT* viewAs() const { return eventCast<const EventT>(evt); @@ -72,8 +60,14 @@ public: return std::exchange(evt, move(other)); } + /// Store arbitrary data with the event item + void setUserData(std::any userData) { data = std::move(userData); } + /// Obtain custom data previously stored with the event item + const std::any& userdata() const { return data; } + std::any& userData() { return data; } + protected: - template <typename EventT> + template <EventClass<RoomEvent> EventT> EventT* getAs() { return eventCast<EventT>(evt); @@ -81,9 +75,10 @@ protected: private: RoomEventPtr evt; + std::any data; }; -class TimelineItem : public EventItemBase { +class QUOTIENT_API TimelineItem : public EventItemBase { public: // For compatibility with Qt containers, even though we use // a std:: container now for the room timeline @@ -100,20 +95,18 @@ private: }; template <> -inline const StateEventBase* EventItemBase::viewAs<StateEventBase>() const +inline const StateEvent* EventItemBase::viewAs<StateEvent>() const { - return evt->isStateEvent() ? weakPtrCast<const StateEventBase>(evt) - : nullptr; + return evt->isStateEvent() ? weakPtrCast<const StateEvent>(evt) : nullptr; } template <> -inline const CallEventBase* EventItemBase::viewAs<CallEventBase>() const +inline const CallEvent* EventItemBase::viewAs<CallEvent>() const { - return evt->isCallEvent() ? weakPtrCast<const CallEventBase>(evt) : nullptr; + return evt->is<CallEvent>() ? weakPtrCast<const CallEvent>(evt) : nullptr; } -class PendingEventItem : public EventItemBase { - Q_GADGET +class QUOTIENT_API PendingEventItem : public EventItemBase { public: using EventItemBase::EventItemBase; @@ -122,7 +115,7 @@ public: QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } - void setFileUploaded(const QUrl& remoteUrl); + void setFileUploaded(const FileSourceInfo &uploadedFileData); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); @@ -155,4 +148,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 a55016d9..324ce449 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -1,47 +1,26 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "converters.h" #include "event.h" -#include "eventcontent.h" namespace Quotient { -constexpr const char* FavouriteTag = "m.favourite"; -constexpr const char* LowPriorityTag = "m.lowpriority"; -constexpr const char* ServerNoticeTag = "m.server_notice"; +constexpr auto FavouriteTag [[maybe_unused]] = "m.favourite"_ls; +constexpr auto LowPriorityTag [[maybe_unused]] = "m.lowpriority"_ls; +constexpr auto ServerNoticeTag [[maybe_unused]] = "m.server_notice"_ls; struct TagRecord { - using order_type = Omittable<float>; - - order_type order; - - TagRecord(order_type order = none) : order(std::move(order)) {} - - bool operator<(const TagRecord& other) const - { - // Per The Spec, rooms with no order should be after those with order, - // against optional<>::operator<() convention. - return order && (!other.order || *order < *other.order); - } + Omittable<float> order = none; }; +inline bool operator<(TagRecord lhs, TagRecord rhs) +{ + // Per The Spec, rooms with no order should be after those with order, + // against std::optional<>::operator<() convention. + return lhs.order && (!rhs.order || *lhs.order < *rhs.order); +} + template <> struct JsonObjectConverter<TagRecord> { static void fillFrom(const QJsonObject& jo, TagRecord& rec) @@ -52,13 +31,13 @@ struct JsonObjectConverter<TagRecord> { if (orderJv.isDouble()) rec.order = fromJson<float>(orderJv); if (orderJv.isString()) { - bool ok; + bool ok = false; rec.order = orderJv.toString().toFloat(&ok); if (!ok) rec.order = none; } } - static void dumpTo(QJsonObject& jo, const TagRecord& rec) + static void dumpTo(QJsonObject& jo, TagRecord rec) { addParam<IfNotEmpty>(jo, QStringLiteral("order"), rec.order); } @@ -66,27 +45,21 @@ struct JsonObjectConverter<TagRecord> { using TagsMap = QHash<QString, TagRecord>; -#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class _Name : public Event { \ - public: \ - using content_type = _ContentType; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(QJsonObject obj) : Event(typeId(), std::move(obj)) {} \ - explicit _Name(_ContentType content) \ - : Event(typeId(), matrixTypeId(), \ - QJsonObject { { QStringLiteral(#_ContentKey), \ - toJson(std::move(content)) } }) \ - {} \ - auto _ContentKey() const \ - { \ - return content<content_type>(#_ContentKey##_ls); \ - } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ - // End of macro - -DEFINE_SIMPLE_EVENT(TagEvent, "m.tag", TagsMap, tags) -DEFINE_SIMPLE_EVENT(ReadMarkerEvent, "m.fully_read", QString, event_id) -DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, "m.ignored_user_list", QSet<QString>, - ignored_users) +DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags, "tags") +DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString, + eventId, "event_id") +class ReadMarkerEvent : public ReadMarkerEventImpl { +public: + using ReadMarkerEventImpl::ReadMarkerEventImpl; + [[deprecated("Use ReadMarkerEvent::eventId() instead")]] + auto event_id() const { return eventId(); } +}; +DEFINE_SIMPLE_EVENT(IgnoredUsersEventImpl, Event, "m.ignored_user_list", + QSet<QString>, ignoredUsers, "ignored_users") +class IgnoredUsersEvent : public IgnoredUsersEventImpl { +public: + using IgnoredUsersEventImpl::IgnoredUsersEventImpl; + [[deprecated("Use IgnoredUsersEvent::ignoredUsers() instead")]] + auto ignored_users() const { return ignoredUsers(); } +}; } // namespace Quotient diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp deleted file mode 100644 index d6622b30..00000000 --- a/lib/events/callanswerevent.cpp +++ /dev/null @@ -1,71 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "callanswerevent.h" - -#include "event.h" -#include "logging.h" - -#include <QtCore/QJsonDocument> - -/* -m.call.answer -{ - "age": 242352, - "content": { - "answer": { - "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", - "type": "answer" - }, - "call_id": "12345", - "lifetime": 60000, - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.answer" -} -*/ - -using namespace Quotient; - -CallAnswerEvent::CallAnswerEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Answer event"; -} - -CallAnswerEvent::CallAnswerEvent(const QString& callId, const int lifetime, - const QString& sdp) - : CallEventBase( - typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("lifetime"), lifetime }, - { QStringLiteral("answer"), - QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } }) -{} - -CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) - : CallEventBase( - typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("answer"), - QJsonObject { { QStringLiteral("type"), QStringLiteral("answer") }, - { QStringLiteral("sdp"), sdp } } } }) -{} diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h deleted file mode 100644 index 2709882b..00000000 --- a/lib/events/callanswerevent.h +++ /dev/null @@ -1,45 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class CallAnswerEvent : public CallEventBase { -public: - DEFINE_EVENT_TYPEID("m.call.answer", CallAnswerEvent) - - explicit CallAnswerEvent(const QJsonObject& obj); - - explicit CallAnswerEvent(const QString& callId, const int lifetime, - const QString& sdp); - explicit CallAnswerEvent(const QString& callId, const QString& sdp); - - int lifetime() const - { - return content<int>("lifetime"_ls); - } // FIXME: Omittable<>? - QString sdp() const - { - return contentJson()["answer"_ls].toObject().value("sdp"_ls).toString(); - } -}; - -REGISTER_EVENT_TYPE(CallAnswerEvent) -} // namespace Quotient diff --git a/lib/events/callcandidatesevent.cpp b/lib/events/callcandidatesevent.cpp deleted file mode 100644 index 24f0dd46..00000000 --- a/lib/events/callcandidatesevent.cpp +++ /dev/null @@ -1,41 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "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 deleted file mode 100644 index e224f048..00000000 --- a/lib/events/callcandidatesevent.h +++ /dev/null @@ -1,45 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class CallCandidatesEvent : public CallEventBase { -public: - DEFINE_EVENT_TYPEID("m.call.candidates", CallCandidatesEvent) - - explicit CallCandidatesEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) - {} - - explicit CallCandidatesEvent(const QString& callId, - const QJsonArray& candidates) - : CallEventBase(typeId(), matrixTypeId(), callId, 0, - { { QStringLiteral("candidates"), candidates } }) - {} - - QJsonArray candidates() const - { - return content<QJsonArray>("candidates"_ls); - } -}; - -REGISTER_EVENT_TYPE(CallCandidatesEvent) -} // namespace Quotient diff --git a/lib/events/callevents.cpp b/lib/events/callevents.cpp new file mode 100644 index 00000000..3873614d --- /dev/null +++ b/lib/events/callevents.cpp @@ -0,0 +1,82 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "callevents.h" + +#include "logging.h" + +using namespace Quotient; + +QJsonObject CallEvent::basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson) +{ + contentJson.insert(QStringLiteral("call_id"), callId); + contentJson.insert(QStringLiteral("version"), version); + return RoomEvent::basicJson(matrixType, contentJson); +} + +CallEvent::CallEvent(const QJsonObject& json) + : RoomEvent(json) +{ + if (callId().isEmpty()) + qCWarning(EVENTS) << id() << "is a call event with an empty call id"; +} + +/* +m.call.invite +{ + "age": 242352, + "content": { + "call_id": "12345", + "lifetime": 60000, + "offer": { + "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", + "type": "offer" + }, + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.invite" +} +*/ + +CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime, + const QString& sdp) + : EventTemplate( + callId, + { { QStringLiteral("lifetime"), lifetime }, + { QStringLiteral("offer"), + QJsonObject{ { QStringLiteral("type"), QStringLiteral("offer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} + +/* +m.call.answer +{ + "age": 242352, + "content": { + "answer": { + "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", + "type": "answer" + }, + "call_id": "12345", + "version": 0 + }, + "event_id": "$WLGTSEFSEF:localhost", + "origin_server_ts": 1431961217939, + "room_id": "!Cuyf34gef24t:localhost", + "sender": "@example:localhost", + "type": "m.call.answer" +} +*/ + +CallAnswerEvent::CallAnswerEvent(const QString& callId, const QString& sdp) + : EventTemplate(callId, { { QStringLiteral("answer"), + QJsonObject { { QStringLiteral("type"), + QStringLiteral("answer") }, + { QStringLiteral("sdp"), sdp } } } }) +{} diff --git a/lib/events/callevents.h b/lib/events/callevents.h new file mode 100644 index 00000000..752e331d --- /dev/null +++ b/lib/events/callevents.h @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" + +namespace Quotient { + +class QUOTIENT_API CallEvent : public RoomEvent { +public: + QUO_BASE_EVENT(CallEvent, "m.call.*"_ls, RoomEvent::BaseMetaType) + static bool matches(const QJsonObject&, const QString& mType) + { + return mType.startsWith("m.call."); + } + + QUO_CONTENT_GETTER(QString, callId) + QUO_CONTENT_GETTER(int, version) + +protected: + explicit CallEvent(const QJsonObject& json); + + static QJsonObject basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson = {}); +}; +using CallEventBase + [[deprecated("CallEventBase is CallEvent now")]] = CallEvent; + +template <typename EventT> +class EventTemplate<EventT, CallEvent> : public CallEvent { +public: + using CallEvent::CallEvent; + explicit EventTemplate(const QString& callId, + const QJsonObject& contentJson = {}) + : EventTemplate(basicJson(EventT::TypeId, callId, 0, contentJson)) + {} +}; + +template <typename EventT, typename ContentT> +class EventTemplate<EventT, CallEvent, ContentT> + : public EventTemplate<EventT, CallEvent> { +public: + using EventTemplate<EventT, CallEvent>::EventTemplate; + template <typename... ContentParamTs> + explicit EventTemplate(const QString& callId, + ContentParamTs&&... contentParams) + : EventTemplate<EventT, CallEvent>( + callId, + toJson(ContentT{ std::forward<ContentParamTs>(contentParams)... })) + {} +}; + +class QUOTIENT_API CallInviteEvent + : public EventTemplate<CallInviteEvent, CallEvent> { +public: + QUO_EVENT(CallInviteEvent, "m.call.invite") + + using EventTemplate::EventTemplate; + + explicit CallInviteEvent(const QString& callId, int lifetime, + const QString& sdp); + + QUO_CONTENT_GETTER(int, lifetime) + QString sdp() const + { + return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString(); + } +}; + +DEFINE_SIMPLE_EVENT(CallCandidatesEvent, CallEvent, "m.call.candidates", + QJsonArray, candidates, "candidates") + +class QUOTIENT_API CallAnswerEvent + : public EventTemplate<CallAnswerEvent, CallEvent> { +public: + QUO_EVENT(CallAnswerEvent, "m.call.answer") + + using EventTemplate::EventTemplate; + + explicit CallAnswerEvent(const QString& callId, const QString& sdp); + + QString sdp() const + { + return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString(); + } +}; + +class QUOTIENT_API CallHangupEvent + : public EventTemplate<CallHangupEvent, CallEvent> { +public: + QUO_EVENT(CallHangupEvent, "m.call.hangup") + using EventTemplate::EventTemplate; +}; + +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::CallEvent*) +Q_DECLARE_METATYPE(const Quotient::CallEvent*) diff --git a/lib/events/callhangupevent.cpp b/lib/events/callhangupevent.cpp deleted file mode 100644 index d41849c3..00000000 --- a/lib/events/callhangupevent.cpp +++ /dev/null @@ -1,52 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "callhangupevent.h" - -#include "event.h" -#include "logging.h" - -#include <QtCore/QJsonDocument> - -/* -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 deleted file mode 100644 index 5d73fb62..00000000 --- a/lib/events/callhangupevent.h +++ /dev/null @@ -1,33 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class CallHangupEvent : public CallEventBase { -public: - DEFINE_EVENT_TYPEID("m.call.hangup", CallHangupEvent) - - explicit CallHangupEvent(const QJsonObject& obj); - explicit CallHangupEvent(const QString& callId); -}; - -REGISTER_EVENT_TYPE(CallHangupEvent) -} // namespace Quotient diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp deleted file mode 100644 index 54faac8d..00000000 --- a/lib/events/callinviteevent.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "callinviteevent.h" - -#include "event.h" -#include "logging.h" - -#include <QtCore/QJsonDocument> - -/* -m.call.invite -{ - "age": 242352, - "content": { - "call_id": "12345", - "lifetime": 60000, - "offer": { - "sdp": "v=0\r\no=- 6584580628695956864 2 IN IP4 127.0.0.1[...]", - "type": "offer" - }, - "version": 0 - }, - "event_id": "$WLGTSEFSEF:localhost", - "origin_server_ts": 1431961217939, - "room_id": "!Cuyf34gef24t:localhost", - "sender": "@example:localhost", - "type": "m.call.invite" -} -*/ - -using namespace Quotient; - -CallInviteEvent::CallInviteEvent(const QJsonObject& obj) - : CallEventBase(typeId(), obj) -{ - qCDebug(EVENTS) << "Call Invite event"; -} - -CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime, - const QString& sdp) - : CallEventBase( - typeId(), matrixTypeId(), callId, lifetime, - { { QStringLiteral("lifetime"), lifetime }, - { QStringLiteral("offer"), - QJsonObject { { QStringLiteral("type"), QStringLiteral("offer") }, - { QStringLiteral("sdp"), sdp } } } }) -{} diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h deleted file mode 100644 index b067a492..00000000 --- a/lib/events/callinviteevent.h +++ /dev/null @@ -1,44 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Marius Gripsgard <marius@ubports.com> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "roomevent.h" - -namespace Quotient { -class CallInviteEvent : public CallEventBase { -public: - DEFINE_EVENT_TYPEID("m.call.invite", CallInviteEvent) - - explicit CallInviteEvent(const QJsonObject& obj); - - explicit CallInviteEvent(const QString& callId, const int lifetime, - const QString& sdp); - - int lifetime() const - { - return content<int>("lifetime"_ls); - } // FIXME: Omittable<>? - QString sdp() const - { - return contentJson()["offer"_ls].toObject().value("sdp"_ls).toString(); - } -}; - -REGISTER_EVENT_TYPE(CallInviteEvent) -} // namespace Quotient diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index b4027e16..83bb1e32 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -1,25 +1,8 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "directchatevent.h" -#include <QtCore/QJsonArray> - using namespace Quotient; QMultiHash<QString, QString> DirectChatEvent::usersToDirectChats() const diff --git a/lib/events/directchatevent.h b/lib/events/directchatevent.h index bb091c5c..0756d816 100644 --- a/lib/events/directchatevent.h +++ b/lib/events/directchatevent.h @@ -1,33 +1,17 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "event.h" namespace Quotient { -class DirectChatEvent : public Event { +class QUOTIENT_API DirectChatEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.direct", DirectChatEvent) + QUO_EVENT(DirectChatEvent, "m.direct") - explicit DirectChatEvent(const QJsonObject& obj) : Event(typeId(), obj) {} + using Event::Event; QMultiHash<QString, QString> usersToDirectChats() const; }; -REGISTER_EVENT_TYPE(DirectChatEvent) } // namespace Quotient diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index dccfa540..540594d1 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -1,32 +1,69 @@ -#include "encryptedevent.h" +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later -#include "room.h" +#include "encryptedevent.h" +#include "e2ee/e2ee.h" +#include "logging.h" using namespace Quotient; -using namespace QtOlm; -EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertext, +EncryptedEvent::EncryptedEvent(const QJsonObject& ciphertexts, const QString& senderKey) - : RoomEvent(typeId(), matrixTypeId(), - { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, - { CiphertextKeyL, ciphertext }, - { SenderKeyKeyL, senderKey } }) + : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, OlmV1Curve25519AesSha2AlgoKey }, + { CiphertextKeyL, ciphertexts }, + { SenderKeyKeyL, senderKey } })) {} -EncryptedEvent::EncryptedEvent(QByteArray ciphertext, const QString& senderKey, +EncryptedEvent::EncryptedEvent(const QByteArray& ciphertext, + const QString& senderKey, const QString& deviceId, const QString& sessionId) - : RoomEvent(typeId(), matrixTypeId(), - { - { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, - { CiphertextKeyL, QString(ciphertext) }, - { DeviceIdKeyL, deviceId }, - { SenderKeyKeyL, senderKey }, - { SessionIdKeyL, sessionId }, - }) + : RoomEvent(basicJson(TypeId, { { AlgorithmKeyL, MegolmV1AesSha2AlgoKey }, + { CiphertextKeyL, QString(ciphertext) }, + { DeviceIdKeyL, deviceId }, + { SenderKeyKeyL, senderKey }, + { SessionIdKeyL, sessionId } })) {} EncryptedEvent::EncryptedEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj) + : RoomEvent(obj) { qCDebug(E2EE) << "Encrypted event from" << senderId(); } + +QString EncryptedEvent::algorithm() const +{ + const auto algo = contentPart<QString>(AlgorithmKeyL); + if (!isSupportedAlgorithm(algo)) + qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo + << "is not supported"; + + return algo; +} + +RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const +{ + auto eventObject = QJsonDocument::fromJson(decrypted.toUtf8()).object(); + eventObject["event_id"] = id(); + eventObject["sender"] = senderId(); + eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch(); + if (const auto relatesToJson = contentPart<QJsonObject>("m.relates_to"_ls); + !relatesToJson.isEmpty()) { + auto content = eventObject["content"].toObject(); + content["m.relates_to"] = relatesToJson; + eventObject["content"] = content; + } + if (const auto redactsJson = unsignedPart<QString>("redacts"_ls); + !redactsJson.isEmpty()) { + auto unsign = eventObject["unsigned"].toObject(); + unsign["redacts"] = redactsJson; + eventObject["unsigned"] = unsign; + } + return loadEvent<RoomEvent>(eventObject); +} + +void EncryptedEvent::setRelation(const QJsonObject& relation) +{ + auto content = contentJson(); + content["m.relates_to"] = relation; + editJson()["content"] = content; +} diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index 235b2aa4..e24e5745 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -1,10 +1,17 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once -#include "e2ee.h" #include "roomevent.h" namespace Quotient { -class Room; + +constexpr auto CiphertextKeyL = "ciphertext"_ls; +constexpr auto SenderKeyKeyL = "sender_key"_ls; +constexpr auto DeviceIdKeyL = "device_id"_ls; +constexpr auto SessionIdKeyL = "session_id"_ls; + /* * While the specification states: * @@ -23,44 +30,39 @@ class Room; * in general. It's possible, because RoomEvent interface is similar to Event's * one and doesn't add new restrictions, just provides additional features. */ -class EncryptedEvent : public RoomEvent { - Q_GADGET +class QUOTIENT_API EncryptedEvent : public RoomEvent { public: - DEFINE_EVENT_TYPEID("m.room.encrypted", EncryptedEvent) + QUO_EVENT(EncryptedEvent, "m.room.encrypted") /* In case with Olm, the encrypted content of the event is * a map from the recipient Curve25519 identity key to ciphertext * information */ - explicit EncryptedEvent(const QJsonObject& ciphertext, + explicit EncryptedEvent(const QJsonObject& ciphertexts, const QString& senderKey); /* In case with Megolm, device_id and session_id are required */ - explicit EncryptedEvent(QByteArray ciphertext, const QString& senderKey, - const QString& deviceId, const QString& sessionId); + explicit EncryptedEvent(const QByteArray& ciphertext, + const QString& senderKey, const QString& deviceId, + const QString& sessionId); explicit EncryptedEvent(const QJsonObject& obj); - QString algorithm() const - { - QString algo = content<QString>(AlgorithmKeyL); - if (!SupportedAlgorithms.contains(algo)) { - qWarning(MAIN) << "The EncryptedEvent's algorithm" << algo - << "is not supported"; - } - return algo; - } + QString algorithm() const; 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); } -}; -REGISTER_EVENT_TYPE(EncryptedEvent) + QString deviceId() const { return contentPart<QString>(DeviceIdKeyL); } + QString sessionId() const { return contentPart<QString>(SessionIdKeyL); } + RoomEventPtr createDecrypted(const QString &decrypted) const; + void setRelation(const QJsonObject& relation); +}; } // namespace Quotient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index f1bde621..b1b04984 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -1,45 +1,53 @@ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "encryptionevent.h" +#include "logging.h" -#include "e2ee.h" +#include "e2ee/e2ee.h" -#include <array> +using namespace Quotient; -namespace Quotient { -static const std::array<QString, 1> encryptionStrings = { - { MegolmV1AesSha2AlgoKey } -}; +static constexpr std::array encryptionStrings { MegolmV1AesSha2AlgoKey }; template <> -struct JsonConverter<EncryptionType> { - static EncryptionType load(const QJsonValue& jv) - { - const auto& encryptionString = jv.toString(); - for (auto it = encryptionStrings.begin(); it != encryptionStrings.end(); - ++it) - if (encryptionString == *it) - return EncryptionType(it - encryptionStrings.begin()); - - if (!encryptionString.isEmpty()) - qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; - return EncryptionType::Undefined; - } -}; -} // namespace Quotient - -using namespace Quotient; +EncryptionType Quotient::fromJson(const QJsonValue& jv) +{ + const auto& encryptionString = jv.toString(); + for (auto it = encryptionStrings.begin(); it != encryptionStrings.end(); + ++it) + if (encryptionString == *it) + return EncryptionType(it - encryptionStrings.begin()); + + if (!encryptionString.isEmpty()) + qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; + return EncryptionType::Undefined; +} EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) - : encryption(fromJson<EncryptionType>(json[AlgorithmKeyL])) + : encryption(fromJson<Quotient::EncryptionType>(json[AlgorithmKeyL])) , algorithm(sanitized(json[AlgorithmKeyL].toString())) - , rotationPeriodMs(json[RotationPeriodMsKeyL].toInt(604800000)) - , rotationPeriodMsgs(json[RotationPeriodMsgsKeyL].toInt(100)) -{} +{ + // NB: fillFromJson only fills the variable if the JSON key exists + fillFromJson<int>(json[RotationPeriodMsKeyL], rotationPeriodMs); + fillFromJson<int>(json[RotationPeriodMsgsKeyL], rotationPeriodMsgs); +} + +EncryptionEventContent::EncryptionEventContent(Quotient::EncryptionType et) + : encryption(et) +{ + if(encryption != Quotient::EncryptionType::Undefined) { + algorithm = encryptionStrings[static_cast<size_t>(encryption)]; + } +} -void EncryptionEventContent::fillJson(QJsonObject* o) const +QJsonObject EncryptionEventContent::toJson() const { - Q_ASSERT(o); - if (encryption != EncryptionType::Undefined) - o->insert(AlgorithmKey, algorithm); - o->insert(RotationPeriodMsKey, rotationPeriodMs); - o->insert(RotationPeriodMsgsKey, rotationPeriodMsgs); + QJsonObject o; + if (encryption != Quotient::EncryptionType::Undefined) + o.insert(AlgorithmKey, algorithm); + o.insert(RotationPeriodMsKey, rotationPeriodMs); + o.insert(RotationPeriodMsgsKey, rotationPeriodMsgs); + return o; } diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h index cbd3ba4a..4bf7459c 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -1,73 +1,47 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "eventcontent.h" +#include "quotient_common.h" #include "stateevent.h" namespace Quotient { -class EncryptionEventContent : public EventContent::Base { +class QUOTIENT_API EncryptionEventContent { public: - enum EncryptionType : size_t { MegolmV1AesSha2 = 0, Undefined }; + using EncryptionType + [[deprecated("Use Quotient::EncryptionType instead")]] = + Quotient::EncryptionType; - explicit EncryptionEventContent(EncryptionType et = Undefined) - : encryption(et) - {} + // NOLINTNEXTLINE(google-explicit-constructor) + QUO_IMPLICIT EncryptionEventContent(Quotient::EncryptionType et); explicit EncryptionEventContent(const QJsonObject& json); - EncryptionType encryption; - QString algorithm; - int rotationPeriodMs; - int rotationPeriodMsgs; + QJsonObject toJson() const; -protected: - void fillJson(QJsonObject* o) const override; + Quotient::EncryptionType encryption; + QString algorithm {}; + int rotationPeriodMs = 604'800'000; + int rotationPeriodMsgs = 100; }; -using EncryptionType = EncryptionEventContent::EncryptionType; - -class EncryptionEvent : public StateEvent<EncryptionEventContent> { - Q_GADGET +class QUOTIENT_API EncryptionEvent + : public KeylessStateEventBase<EncryptionEvent, EncryptionEventContent> { public: - DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) - - using EncryptionType = EncryptionEventContent::EncryptionType; + QUO_EVENT(EncryptionEvent, "m.room.encryption") - explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate - // default value - : StateEvent(typeId(), obj) - {} - template <typename... ArgTs> - EncryptionEvent(ArgTs&&... contentArgs) - : StateEvent(typeId(), matrixTypeId(), QString(), - std::forward<ArgTs>(contentArgs)...) - {} + using EncryptionType + [[deprecated("Use Quotient::EncryptionType instead")]] = + Quotient::EncryptionType; - EncryptionType encryption() const { return content().encryption; } + using KeylessStateEventBase::KeylessStateEventBase; + Quotient::EncryptionType encryption() const { return content().encryption; } QString algorithm() const { return content().algorithm; } int rotationPeriodMs() const { return content().rotationPeriodMs; } int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } -private: - Q_ENUM(EncryptionType) + bool useEncryption() const { return !algorithm().isEmpty(); } }; - -REGISTER_EVENT_TYPE(EncryptionEvent) } // namespace Quotient diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 7b34114d..da7de919 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -1,48 +1,54 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "event.h" +#include "callevents.h" #include "logging.h" +#include "stateevent.h" #include <QtCore/QJsonDocument> using namespace Quotient; -event_type_t EventTypeRegistry::initializeTypeId(event_mtype_t matrixTypeId) -{ - const auto id = get().eventTypes.size(); - get().eventTypes.push_back(matrixTypeId); - if (strncmp(matrixTypeId, "", 1) == 0) - qDebug(EVENTS) << "Initialized unknown event type with id" << id; - else - qDebug(EVENTS) << "Initialized event type" << matrixTypeId << "with id" - << id; - return id; -} +QString EventTypeRegistry::getMatrixType(event_type_t typeId) { return typeId; } -QString EventTypeRegistry::getMatrixType(event_type_t typeId) +void AbstractEventMetaType::addDerived(AbstractEventMetaType* newType) { - return typeId < get().eventTypes.size() ? get().eventTypes[typeId] - : QString(); + if (const auto existing = + std::find_if(derivedTypes.cbegin(), derivedTypes.cend(), + [&newType](const AbstractEventMetaType* t) { + return t->matrixId == newType->matrixId; + }); + existing != derivedTypes.cend()) + { + if (*existing == newType) + return; + // Two different metatype objects claim the same Matrix type id; this + // is not normal, so give as much information as possible to diagnose + if ((*existing)->className == newType->className) { + qCritical(EVENTS) + << newType->className << "claims" << newType->matrixId + << "repeatedly; check that it's exported across translation " + "units or shared objects"; + Q_ASSERT(false); // That situation is plain wrong + return; // So maybe std::terminate() even? + } + qWarning(EVENTS).nospace() + << newType->matrixId << " is already mapped to " + << (*existing)->className << " before " << newType->className + << "; unless the two have different isValid() conditions, the " + "latter class will never be used"; + } + derivedTypes.emplace_back(newType); + qDebug(EVENTS).nospace() + << newType->matrixId << " -> " << newType->className << "; " + << derivedTypes.size() << " derived type(s) registered for " + << className; } -Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) +Event::Event(const QJsonObject& json) + : _json(json) { if (!json.contains(ContentKeyL) && !json.value(UnsignedKeyL).toObject().contains(RedactedCauseKeyL)) { @@ -51,29 +57,26 @@ Event::Event(Type type, const QJsonObject& json) : _type(type), _json(json) } } -Event::Event(Type type, event_mtype_t matrixType, const QJsonObject& contentJson) - : Event(type, basicEventJson(matrixType, contentJson)) -{} - Event::~Event() = default; QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } 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(); } +bool Event::isStateEvent() const { return is<StateEvent>(); } + +bool Event::isCallEvent() const { return is<CallEvent>(); } + void Event::dumpTo(QDebug dbg) const { dbg << QJsonDocument(contentJson()).toJson(QJsonDocument::Compact); diff --git a/lib/events/event.h b/lib/events/event.h index 6c8961ad..0abef1f0 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -1,32 +1,14 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "converters.h" -#include "logging.h" - -#ifdef ENABLE_EVENTTYPE_ALIAS -# define USE_EVENTTYPE_ALIAS 1 -#endif +#include "function_traits.h" +#include "single_key_value.h" namespace Quotient { -// === event_ptr_tt<> and type casting facilities === +// === event_ptr_tt<> and basic type casting facilities === template <typename EventT> using event_ptr_tt = std::unique_ptr<EventT>; @@ -45,198 +27,288 @@ inline TargetEventT* weakPtrCast(const event_ptr_tt<EventT>& ptr) return static_cast<TargetEventT*>(rawPtr(ptr)); } -/// Re-wrap a smart pointer to base into a smart pointer to derived -template <typename TargetT, typename SourceT> -[[deprecated("Consider using eventCast() or visit() instead")]] -inline event_ptr_tt<TargetT> ptrCast(event_ptr_tt<SourceT>&& ptr) -{ - return std::unique_ptr<TargetT>(static_cast<TargetT*>(ptr.release())); -} - // === Standard Matrix key names and basicEventJson() === -static const auto TypeKey = QStringLiteral("type"); -static const auto BodyKey = QStringLiteral("body"); -static const auto ContentKey = QStringLiteral("content"); -static const auto EventIdKey = QStringLiteral("event_id"); -static const auto UnsignedKey = QStringLiteral("unsigned"); -static const auto StateKeyKey = QStringLiteral("state_key"); -static const auto TypeKeyL = "type"_ls; -static const auto BodyKeyL = "body"_ls; -static const auto ContentKeyL = "content"_ls; -static const auto EventIdKeyL = "event_id"_ls; -static const auto UnsignedKeyL = "unsigned"_ls; -static const auto RedactedCauseKeyL = "redacted_because"_ls; -static const auto PrevContentKeyL = "prev_content"_ls; -static const auto StateKeyKeyL = "state_key"_ls; - -/// Make a minimal correct Matrix event JSON -template <typename StrT> -inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content) -{ - return { { TypeKey, std::forward<StrT>(matrixType) }, - { ContentKey, content } }; -} - -// === Event types and event types registry === - -using event_type_t = size_t; -using event_mtype_t = const char*; +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 }; + +using event_type_t = QLatin1String; + +// TODO: Remove in 0.8 +struct QUOTIENT_API EventTypeRegistry { + [[deprecated("event_type_t is a string since libQuotient 0.7, use it directly instead")]] + static QString getMatrixType(event_type_t typeId); -class EventTypeRegistry { -public: + EventTypeRegistry() = delete; ~EventTypeRegistry() = default; + Q_DISABLE_COPY_MOVE(EventTypeRegistry) +}; - static event_type_t initializeTypeId(event_mtype_t matrixTypeId); +// === EventMetaType === - template <typename EventT> - static inline event_type_t initializeTypeId() - { - return initializeTypeId(EventT::matrixTypeId()); - } +class Event; - static QString getMatrixType(event_type_t typeId); +// TODO: move over to std::derived_from<Event> once it's available everywhere +template <typename EventT, typename BaseEventT = Event> +concept EventClass = std::is_base_of_v<BaseEventT, EventT>; -private: - EventTypeRegistry() = default; - Q_DISABLE_COPY(EventTypeRegistry) - DISABLE_MOVE(EventTypeRegistry) +template <EventClass EventT> +bool is(const Event& e); - static EventTypeRegistry& get() +//! \brief The base class for event metatypes +//! +//! You should not normally have to use this directly, unless you need to devise +//! a whole new kind of event metatypes. +class QUOTIENT_API AbstractEventMetaType { +public: + // The public fields here are const and are not to be changeable anyway. + // NOLINTBEGIN(misc-non-private-member-variables-in-classes) + const char* const className; + const event_type_t matrixId; + const AbstractEventMetaType* const baseType = nullptr; + // NOLINTEND(misc-non-private-member-variables-in-classes) + + explicit AbstractEventMetaType(const char* className) + : className(className) + {} + explicit AbstractEventMetaType(const char* className, event_type_t matrixId, + AbstractEventMetaType& nearestBase) + : className(className), matrixId(matrixId), baseType(&nearestBase) { - static EventTypeRegistry etr; - return etr; + nearestBase.addDerived(this); } - std::vector<event_mtype_t> eventTypes; -}; - -template <> -inline event_type_t EventTypeRegistry::initializeTypeId<void>() -{ - return initializeTypeId(""); -} + void addDerived(AbstractEventMetaType *newType); -template <typename EventT> -struct EventTypeTraits { - static event_type_t id() - { - static const auto id = EventTypeRegistry::initializeTypeId<EventT>(); - return id; - } -}; + virtual ~AbstractEventMetaType() = default; -template <typename EventT> -inline event_type_t typeId() -{ - return EventTypeTraits<std::decay_t<EventT>>::id(); -} +protected: + // Allow template specialisations to call into one another + template <class EventT> + friend class EventMetaType; -inline event_type_t unknownEventTypeId() { return typeId<void>(); } + // The returned value indicates whether a generic object has to be created + // on the top level when `event` is empty, instead of returning nullptr + virtual bool doLoadFrom(const QJsonObject& fullJson, const QString& type, + Event*& event) const = 0; -// === EventFactory === +private: + std::vector<const AbstractEventMetaType*> derivedTypes{}; + Q_DISABLE_COPY_MOVE(AbstractEventMetaType) +}; -/** Create an event of arbitrary type from its arguments */ -template <typename EventT, typename... ArgTs> -inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args) +// Any event metatype is unique (note Q_DISABLE_COPY_MOVE above) so can be +// identified by its address +inline bool operator==(const AbstractEventMetaType& lhs, + const AbstractEventMetaType& rhs) { - return std::make_unique<EventT>(std::forward<ArgTs>(args)...); + return &lhs == &rhs; } -template <typename BaseEventT> -class EventFactory { +//! \brief A family of event meta-types to load and match events +//! +//! TL;DR for the loadFrom() story: +//! - for base event types, use QUO_BASE_EVENT and, if you have additional +//! validation (e.g., JSON has to contain a certain key - see StateEvent +//! for a real example), define it in the static EventT::isValid() member +//! function accepting QJsonObject and returning bool. +//! - for leaf (specific) event types - simply use QUO_EVENT and it will do +//! everything necessary, including the TypeId definition. +//! \sa QUO_EVENT, QUO_BASE_EVENT +template <class EventT> +class QUOTIENT_API EventMetaType : public AbstractEventMetaType { + // Above: can't constrain EventT to be EventClass because it's incomplete + // at the point of EventMetaType<EventT> instantiation. 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() + using AbstractEventMetaType::AbstractEventMetaType; + + //! \brief Try to load an event from JSON, with dynamic type resolution + //! + //! The generic logic defined in this class template and invoked applies to + //! all event types defined in the library and boils down to the following: + //! 1. + //! a. If EventT has TypeId defined (which normally is a case of + //! all leaf - specific - event types, via QUO_EVENT macro) and + //! \p type doesn't exactly match it, nullptr is immediately returned. + //! b. In absence of TypeId, an event type is assumed to be a base; + //! its derivedTypes are examined, and this algorithm is applied + //! recursively on each. + //! 2. Optional validation: if EventT (or, due to the way inheritance works, + //! any of its base event types) has a static isValid() predicate and + //! the event JSON does not satisfy it, nullptr is immediately returned + //! to the upper level or to the loadFrom() caller. This is how existence + //! of `state_key` is checked in any type derived from StateEvent. + //! 3. If step 1b above returned non-nullptr, immediately return it. + //! 4. + //! a. If EventT::isValid() or EventT::TypeId (either, or both) exist and + //! are satisfied (see steps 1a and 2 above), an object of this type + //! is created from the passed JSON and returned. In case of a base + //! event type, this will be a generic (aka "unknown") event. + //! b. If neither exists, a generic event is only created and returned + //! when on the top level (i.e., outside of recursion into + //! derivedTypes); lower levels return nullptr instead and the type + //! lookup continues. The latter is a case of a derived base event + //! metatype (e.g. RoomEvent) called from its base event metatype + //! (i.e., Event). If no matching type derived from RoomEvent is found, + //! the nested lookup returns nullptr rather than a generic RoomEvent, + //! so that other types derived from Event could be examined. + event_ptr_tt<EventT> loadFrom(const QJsonObject& fullJson, + const QString& type) const { - 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; + Event* event = nullptr; + const bool goodEnough = doLoadFrom(fullJson, type, event); + if (!event && goodEnough) + return event_ptr_tt<EventT>{ new EventT(fullJson) }; + return event_ptr_tt<EventT>{ static_cast<EventT*>(event) }; } private: - static auto& factories() + bool doLoadFrom(const QJsonObject& fullJson, const QString& type, + Event*& event) const override { - using inner_factory_tt = std::function<event_ptr_tt<BaseEventT>( - const QJsonObject&, const QString&)>; - static std::vector<inner_factory_tt> _factories {}; - return _factories; + if constexpr (requires { EventT::TypeId; }) { + if (EventT::TypeId != type) + return false; + } else { + for (const auto& p : derivedTypes) { + p->doLoadFrom(fullJson, type, event); + if (event) { + Q_ASSERT(is<EventT>(*event)); + return false; + } + } + } + if constexpr (requires { EventT::isValid; }) { + if (!EventT::isValid(fullJson)) + return false; + } else if constexpr (!requires { EventT::TypeId; }) + return true; // Create a generic event object if on the top level + event = new EventT(fullJson); + return false; } }; -/** 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() +// === Event creation facilities === + +//! \brief Create an event of arbitrary type from its arguments +//! +//! This should not be used to load events from JSON - use loadEvent() for that. +//! \sa loadEvent +template <EventClass EventT, typename... ArgTs> +inline event_ptr_tt<EventT> makeEvent(ArgTs&&... args) { - qDebug(EVENTS) << "Adding factory method for" << EventT::matrixTypeId(); - return EventT::factory_t::addMethod([](const QJsonObject& json, - const QString& jsonMatrixType) { - return EventT::matrixTypeId() == jsonMatrixType ? makeEvent<EventT>(json) - : nullptr; - }); + return std::make_unique<EventT>(std::forward<ArgTs>(args)...); } -template <typename EventT> -inline auto registerEventType() +template <EventClass EventT> +constexpr const auto& mostSpecificMetaType() { - // 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 + if constexpr (requires { EventT::MetaType; }) + return EventT::MetaType; + else + return EventT::BaseMetaType; } +//! \brief Create an event with proper type from a JSON object +//! +//! Use this factory template to detect the type from the JSON object +//! contents (the detected event type should derive from the template +//! parameter type) and create an event object of that type. +template <EventClass EventT> +inline event_ptr_tt<EventT> loadEvent(const QJsonObject& fullJson) +{ + return mostSpecificMetaType<EventT>().loadFrom( + fullJson, fullJson[TypeKeyL].toString()); +} + +//! \brief Create an event from a type string and content JSON +//! +//! Use this template to resolve the C++ type from the Matrix type string in +//! \p matrixType and create an event of that type by passing all parameters +//! to BaseEventT::basicJson(). +template <EventClass EventT> +inline event_ptr_tt<EventT> loadEvent(const QString& matrixType, + const auto&... otherBasicJsonParams) +{ + return mostSpecificMetaType<EventT>().loadFrom( + EventT::basicJson(matrixType, otherBasicJsonParams...), matrixType); +} + +template <EventClass EventT> +struct JsonConverter<event_ptr_tt<EventT>> + : JsonObjectUnpacker<event_ptr_tt<EventT>> { + // No dump() to avoid any ambiguity on whether a given export to JSON uses + // fullJson() or only contentJson() + using JsonObjectUnpacker<event_ptr_tt<EventT>>::load; + static auto load(const QJsonObject& jo) + { + return loadEvent<EventT>(jo); + } +}; + // === Event === -class Event { - Q_GADGET - Q_PROPERTY(Type type READ type CONSTANT) - Q_PROPERTY(QJsonObject contentJson READ contentJson CONSTANT) +class QUOTIENT_API Event { public: using Type = event_type_t; - using factory_t = EventFactory<Event>; + static inline EventMetaType<Event> BaseMetaType { "Event" }; + virtual const AbstractEventMetaType& metaType() const + { + return BaseMetaType; + } - explicit Event(Type type, const QJsonObject& json); - explicit Event(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); Q_DISABLE_COPY(Event) - Event(Event&&) = default; + Event(Event&&) noexcept = default; Event& operator=(Event&&) = delete; virtual ~Event(); - Type type() const { return _type; } + /// Make a minimal correct Matrix event JSON + static QJsonObject basicJson(const QString& matrixType, + const QJsonObject& content) + { + return { { TypeKey, matrixType }, { ContentKey, content } }; + } + + //! \brief Event Matrix type, as identified by its metatype object + //! + //! For generic/unknown events it will contain a descriptive/generic string + //! defined by the respective base event type (that can be empty). + //! \sa matrixType + Type type() const { return metaType().matrixId; } + + //! \brief Exact Matrix type stored in JSON + //! + //! Coincides with the result of type() (but is slower) for events defined + //! in C++ (not necessarily in the library); for generic/unknown events + //! the returned value will be different. QString matrixType() const; + + template <EventClass EventT> + bool is() const + { + return Quotient::is<EventT>(*this); + } + + [[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; } @@ -245,148 +317,320 @@ 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; + + //! \brief Get a part of the content object, assuming a given type + //! + //! This retrieves the value under `content.<key>` from the event JSON and + //! then converts it to \p T using fromJson(). + //! \sa contentJson, fromJson + template <typename T, 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; + + //! \brief Get a part of the unsigned object, assuming a given type + //! + //! This retrieves the value under `unsigned.<key>` from the event JSON and + //! then converts it to \p T using fromJson(). + //! \sa unsignedJson, fromJson + template <typename T, 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) + friend QUOTIENT_API QDebug operator<<(QDebug dbg, const Event& e) { QDebugStateSaver _dss { dbg }; - dbg.noquote().nospace() << e.matrixType() << '(' << e.type() << "): "; + dbg.noquote().nospace() + << e.matrixType() << '(' << e.metaType().className << "): "; e.dumpTo(dbg); return dbg; } - virtual bool isStateEvent() const { return false; } - virtual bool isCallEvent() const { return false; } - virtual void dumpTo(QDebug dbg) const; + // State events are quite special in Matrix; so isStateEvent() is here, + // as an exception. For other base events, Event::is<>() and + // Quotient::is<>() should be used; don't add is* methods here + bool isStateEvent() const; + [[deprecated("Use is<CallEvent>() instead")]] bool isCallEvent() const; protected: + friend class EventMetaType<Event>; // To access the below constructor + + explicit Event(const QJsonObject& json); + QJsonObject& editJson() { return _json; } + virtual void dumpTo(QDebug dbg) const; private: - Type _type; QJsonObject _json; }; using EventPtr = event_ptr_tt<Event>; -template <typename EventT> +template <EventClass EventT> using EventsArray = std::vector<event_ptr_tt<EventT>>; using Events = EventsArray<Event>; -// === Macros used with event class definitions === +// === Facilities for event class definitions === + +//! \brief A template base class to derive your event type from +//! +//! This simple class template generates commonly used event constructor +//! signatures and the content() method with the appropriate return type. +//! The generic version here is only used with non-trivial \p ContentT (if you +//! don't need to create an event from its content structure, just go and derive +//! straight from the respective \p EventBaseT instead of using EventTemplate); +//! specialisations may override that and provide useful semantics even without +//! \p ContentT (see EventTemplate<CallEvent>, e.g.). +//! +//! The template uses CRTP to pick the event type id from the actual class; +//! it will fail to compile if \p EventT doesn't provide TypeId. It also uses +//! the base event type's basicJson(); if you need extra keys to be inserted +//! you may want to bypass this template as writing the code to that effect in +//! your class will likely be clearer and more concise. +//! \sa https://en.wikipedia.org/wiki/Curiously_recurring_template_pattern +//! \sa DEFINE_SIMPLE_EVENT +template <typename EventT, EventClass BaseEventT, typename ContentT = void> +class EventTemplate : public BaseEventT { + // Above: can't constrain EventT to be EventClass because it's incomplete + // by CRTP definition. +public: + static_assert( + !std::is_same_v<ContentT, void>, + "If you see this, you tried to use EventTemplate with the default" + " ContentT type, which is void. This default is only used with explicit" + " specialisations (see CallEvent, e.g.). Otherwise, if you don't intend" + " to use the content part of EventTemplate then you don't need" + " EventTemplate; just use the base event class directly"); + using content_type = ContentT; + + explicit EventTemplate(const QJsonObject& json) + : BaseEventT(json) + {} + explicit EventTemplate(const ContentT& c) + : BaseEventT(EventT::basicJson(EventT::TypeId, toJson(c))) + {} + + ContentT content() const { return fromJson<ContentT>(this->contentJson()); } +}; -// This macro should be used in a public section of an event class to -// provide matrixTypeId() and typeId(). -#define DEFINE_EVENT_TYPEID(_Id, _Type) \ - static constexpr event_mtype_t matrixTypeId() { return _Id; } \ - static auto typeId() { return Quotient::typeId<_Type>(); } \ +//! \brief Supply event metatype information in base event types +//! +//! Use this macro in a public section of your base event class to provide +//! type identity and enable dynamic loading of generic events of that type. +//! Do _not_ add this macro if your class is an intermediate wrapper and is not +//! supposed to be instantiated on its own. Provides BaseMetaType static field +//! initialised by parameters passed to the macro, and a metaType() override +//! pointing to that BaseMetaType. +//! \sa EventMetaType, EventMetaType::SuppressLoadDerived +#define QUO_BASE_EVENT(CppType_, ...) \ + friend class EventMetaType<CppType_>; \ + static inline EventMetaType<CppType_> BaseMetaType{ \ + #CppType_ __VA_OPT__(,) __VA_ARGS__ }; \ + const AbstractEventMetaType& metaType() const override \ + { \ + return BaseMetaType; \ + } \ // End of macro -// 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>(); \ - } \ +//! Supply event metatype information in (specific) event types +//! +//! Use this macro in a public section of your event class to provide type +//! identity and enable dynamic loading of generic events of that type. +//! Do _not_ use this macro if your class is an intermediate wrapper and is not +//! supposed to be instantiated on its own. Provides MetaType static field +//! initialised as described below; a metaType() override pointing to it; and +//! the TypeId static field that is equal to MetaType.matrixId. +//! +//! The first two macro parameters are used as the first two EventMetaType +//! constructor parameters; the third EventMetaType parameter is always +//! BaseMetaType; and additional base types can be passed in extra macro +//! parameters if you need to include the same event type in more than one +//! event factory hierarchy (e.g., EncryptedEvent). +//! \sa EventMetaType +#define QUO_EVENT(CppType_, MatrixType_, ...) \ + static inline const auto& TypeId = MatrixType_##_ls; \ + friend class EventMetaType<CppType_>; \ + static inline const EventMetaType<CppType_> MetaType{ \ + #CppType_, TypeId, BaseMetaType __VA_OPT__(,) __VA_ARGS__ \ + }; \ + const AbstractEventMetaType& metaType() const override \ + { \ + return MetaType; \ + } \ + [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \ + static constexpr const char* matrixTypeId() { return MatrixType_; } \ + [[deprecated("Use " #CppType_ "::TypeId directly instead")]] \ + static event_type_t typeId() { return TypeId; } \ // End of macro -// === is<>(), eventCast<>() and visit<>() === +//! \deprecated This is the old name for what is now known as QUO_EVENT +#define DEFINE_EVENT_TYPEID(Type_, Id_) QUO_EVENT(Type_, Id_) -template <typename EventT> -inline bool is(const Event& e) -{ - return e.type() == typeId<EventT>(); -} +#define QUO_CONTENT_GETTER_X(PartType_, PartName_, JsonKey_) \ + PartType_ PartName_() const \ + { \ + static const auto PartName_##JsonKey = JsonKey_; \ + return contentPart<PartType_>(PartName_##JsonKey); \ + } -inline bool isUnknown(const Event& e) +//! \brief Define an inline method obtaining a content part +//! +//! This macro adds a const method that extracts a JSON value at the key +//! <tt>toSnakeCase(PartName_)</tt> (sic) and converts it to the type +//! \p PartType_. Effectively, the generated method is an equivalent of +//! \code +//! contentPart<PartType_>(Quotient::toSnakeCase(#PartName_##_ls)); +//! \endcode +#define QUO_CONTENT_GETTER(PartType_, PartName_) \ + QUO_CONTENT_GETTER_X(PartType_, PartName_, toSnakeCase(#PartName_##_ls)) + +//! \deprecated This macro was used after an event class definition +//! to enable its dynamic loading; it is completely superseded by QUO_EVENT +#define REGISTER_EVENT_TYPE(Type_) + +/// \brief Define a new event class with a single key-value pair in the content +/// +/// This macro defines a new event class \p Name_ derived from \p Base_, +/// with Matrix event type \p TypeId_, providing a getter named \p GetterName_ +/// for a single value of type \p ValueType_ inside the event content. +/// To retrieve the value the getter uses a JSON key name that corresponds to +/// its own (getter's) name but written in snake_case. \p GetterName_ must be +/// in camelCase, no quotes (an identifier, not a literal). +#define DEFINE_SIMPLE_EVENT(Name_, Base_, TypeId_, ValueType_, GetterName_, \ + JsonKey_) \ + constexpr auto Name_##ContentKey = JsonKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public EventTemplate< \ + Name_, Base_, \ + EventContent::SingleKeyValue<ValueType_, Name_##ContentKey>> { \ + public: \ + QUO_EVENT(Name_, TypeId_) \ + using value_type = ValueType_; \ + using EventTemplate::EventTemplate; \ + QUO_CONTENT_GETTER_X(ValueType_, GetterName_, Name_##ContentKey) \ + }; \ + // End of macro + +// === is<>(), eventCast<>() and switchOnType<>() === + +template <EventClass EventT> +inline bool is(const Event& e) { - return e.type() == unknownEventTypeId(); + if constexpr (requires { EventT::MetaType; }) { + return &e.metaType() == &EventT::MetaType; + } else { + const auto* p = &e.metaType(); + do { + if (p == &EventT::BaseMetaType) + return true; + } while ((p = p->baseType) != nullptr); + return false; + } } -template <typename EventT, typename BasePtrT> +//! \brief Cast the event pointer down in a type-safe way +//! +//! Checks that the event \p eptr points to actually is of the requested type +//! and returns a (plain) pointer to the event downcast to that type. \p eptr +//! can be either "dumb" (BaseEventT*) or "smart" (`event_ptr_tt<>`). This +//! overload doesn't affect the event ownership - if the original pointer owns +//! the event it must outlive the downcast pointer to keep it from dangling. +template <EventClass EventT, typename BasePtrT> inline auto eventCast(const BasePtrT& eptr) -> decltype(static_cast<EventT*>(&*eptr)) { - Q_ASSERT(eptr); - return is<std::decay_t<EventT>>(*eptr) ? static_cast<EventT*>(&*eptr) - : nullptr; + return eptr && is<std::decay_t<EventT>>(*eptr) + ? static_cast<EventT*>(&*eptr) + : nullptr; } -// A single generic catch-all visitor -template <typename BaseEventT, typename FnT> -inline auto visit(const BaseEventT& event, FnT&& visitor) - -> decltype(visitor(event)) +//! \brief Cast the event pointer down in a type-safe way, with moving +//! +//! Checks that the event \p eptr points to actually is of the requested type; +//! if (and only if) it is, releases the pointer, downcasts it to the requested +//! event type and returns a new smart pointer wrapping the downcast one. +//! Unlike the non-moving eventCast() overload, this one only accepts a smart +//! pointer, and that smart pointer should be an rvalue (either a temporary, +//! or as a result of std::move()). The ownership, respectively, is transferred +//! to the new pointer; the original smart pointer is reset to nullptr, as is +//! normal for `unique_ptr<>::release()`. +//! \note If \p eptr's event type does not match \p EventT it retains ownership +//! after calling this overload; if it is a temporary, this normally +//! leads to the event getting deleted along with the end of +//! the temporary's lifetime. +template <EventClass EventT, typename BaseEventT> +inline auto eventCast(event_ptr_tt<BaseEventT>&& eptr) { - return visitor(event); + return eptr && is<std::decay_t<EventT>>(*eptr) + ? event_ptr_tt<EventT>(static_cast<EventT*>(eptr.release())) + : nullptr; } namespace _impl { - template <typename T, typename FnT> - constexpr auto needs_downcast() + template <typename FnT, typename BaseT> + concept Invocable_With_Downcast = requires { - return !std::is_convertible_v<T, fn_arg_t<FnT>>; - } + requires EventClass<BaseT>; + std::is_base_of_v<BaseT, std::remove_cvref_t<fn_arg_t<FnT>>>; + }; } -// A single type-specific void visitor -template <typename BaseEventT, typename FnT> -inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>() - && std::is_void_v<fn_return_t<FnT>>> -visit(const BaseEventT& event, FnT&& visitor) +template <EventClass BaseT, typename TailT> +inline auto switchOnType(const BaseT& event, TailT&& tail) { - using event_type = fn_arg_t<FnT>; - if (is<std::decay_t<event_type>>(event)) - visitor(static_cast<event_type>(event)); + if constexpr (std::is_invocable_v<TailT, BaseT>) { + return tail(event); + } else if constexpr (_impl::Invocable_With_Downcast<TailT, BaseT>) { + using event_type = fn_arg_t<TailT>; + if (is<std::decay_t<event_type>>(event)) + return tail(static_cast<event_type>(event)); + return std::invoke_result_t<TailT, event_type>(); // Default-constructed + } else { // Treat it as a value to return + return std::forward<TailT>(tail); + } } -// A single type-specific non-void visitor with an optional default value -// non-voidness is guarded by defaultValue type -template <typename BaseEventT, typename FnT> -inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>(), fn_return_t<FnT>> -visit(const BaseEventT& event, FnT&& visitor, - fn_return_t<FnT>&& defaultValue = {}) +template <EventClass BaseT, typename FnT1, typename... FnTs> +inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns) { - 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); + using event_type1 = fn_arg_t<FnT1>; + if (is<std::decay_t<event_type1>>(event)) + return fn1(static_cast<event_type1>(event)); + return switchOnType(event, std::forward<FnTs>(fns)...); } -// A chain of 2 or more visitors -template <typename BaseEventT, typename FnT1, typename FnT2, typename... FnTs> -inline fn_return_t<FnT1> visit(const BaseEventT& event, FnT1&& visitor1, - FnT2&& visitor2, FnTs&&... visitors) +template <EventClass BaseT, typename... FnTs> +[[deprecated("The new name for visit() is switchOnType()")]] // +inline auto visit(const BaseT& event, 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 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) - -> std::enable_if_t<std::is_convertible_v< - std::decay_t<decltype(**events.begin())>, Event>> +inline auto visitEach(RangeT&& events, FnTs&&... fns) + requires std::is_void_v< + 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 802d8176..8db3b7e3 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -1,53 +1,55 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "eventcontent.h" #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 { QJsonObject o; - fillJson(&o); + fillJson(o); return o; } -FileInfo::FileInfo(const QUrl& u, qint64 payloadSize, const QMimeType& mimeType, - const QString& originalFilename) - : mimeType(mimeType) - , url(u) +FileInfo::FileInfo(const QFileInfo& fi) + : source(QUrl::fromLocalFile(fi.filePath())), + mimeType(QMimeDatabase().mimeTypeForFile(fi)), + payloadSize(fi.size()), + originalName(fi.fileName()) +{ + Q_ASSERT(fi.isFile()); +} + +FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize, + const QMimeType& mimeType, QString originalFilename) + : source(move(sourceInfo)) + , mimeType(mimeType) , payloadSize(payloadSize) - , originalName(originalFilename) -{} + , originalName(move(originalFilename)) +{ + 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) - : originalInfoJson(infoJson) +FileInfo::FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + QString originalFilename) + : source(move(sourceInfo)) + , originalInfoJson(infoJson) , mimeType( QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) - , url(u) , payloadSize(fromJson<qint64>(infoJson["size"_ls])) - , originalName(originalFilename) + , originalName(move(originalFilename)) { if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); @@ -55,49 +57,66 @@ FileInfo::FileInfo(const QUrl& u, const QJsonObject& infoJson, bool FileInfo::isValid() const { - return url.scheme() == "mxc" - && (url.authority() + url.path()).count('/') == 1; + const auto& u = url(); + return u.scheme() == "mxc" && (u.authority() + u.path()).count('/') == 1; } -void FileInfo::fillInfoJson(QJsonObject* infoJson) const +QUrl FileInfo::url() const { - Q_ASSERT(infoJson); - if (payloadSize != -1) - infoJson->insert(QStringLiteral("size"), payloadSize); - if (mimeType.isValid()) - infoJson->insert(QStringLiteral("mimetype"), mimeType.name()); + return getUrlFromSourceInfo(source); } -ImageInfo::ImageInfo(const QUrl& u, qint64 fileSize, QMimeType mimeType, - const QSize& imageSize, const QString& originalFilename) - : FileInfo(u, fileSize, mimeType, originalFilename), imageSize(imageSize) +QJsonObject Quotient::EventContent::toInfoJson(const FileInfo& info) +{ + QJsonObject infoJson; + if (info.payloadSize != -1) + infoJson.insert(QStringLiteral("size"), info.payloadSize); + if (info.mimeType.isValid()) + infoJson.insert(QStringLiteral("mimetype"), info.mimeType.name()); + return infoJson; +} + +ImageInfo::ImageInfo(const QFileInfo& fi, QSize imageSize) + : FileInfo(fi), imageSize(imageSize) +{} + +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize, + const QMimeType& type, QSize imageSize, + const QString& originalFilename) + : FileInfo(move(sourceInfo), fileSize, type, originalFilename) + , imageSize(imageSize) {} -ImageInfo::ImageInfo(const QUrl& u, const QJsonObject& infoJson, +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, const QString& originalFilename) - : FileInfo(u, infoJson, originalFilename) + : FileInfo(move(sourceInfo), infoJson, originalFilename) , imageSize(infoJson["w"_ls].toInt(), infoJson["h"_ls].toInt()) {} -void ImageInfo::fillInfoJson(QJsonObject* infoJson) const +QJsonObject Quotient::EventContent::toInfoJson(const ImageInfo& info) { - FileInfo::fillInfoJson(infoJson); - if (imageSize.width() != -1) - infoJson->insert(QStringLiteral("w"), imageSize.width()); - if (imageSize.height() != -1) - infoJson->insert(QStringLiteral("h"), imageSize.height()); + auto infoJson = toInfoJson(static_cast<const FileInfo&>(info)); + if (info.imageSize.width() != -1) + infoJson.insert(QStringLiteral("w"), info.imageSize.width()); + if (info.imageSize.height() != -1) + infoJson.insert(QStringLiteral("h"), info.imageSize.height()); + return infoJson; } -Thumbnail::Thumbnail(const QJsonObject& infoJson) - : ImageInfo(infoJson["thumbnail_url"_ls].toString(), +Thumbnail::Thumbnail(const QJsonObject& infoJson, + const Omittable<EncryptedFileMetadata>& efm) + : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), infoJson["thumbnail_info"_ls].toObject()) -{} +{ + if (efm) + source = *efm; +} -void Thumbnail::fillInfoJson(QJsonObject* infoJson) const +void Thumbnail::dumpTo(QJsonObject& infoJson) const { - if (url.isValid()) - infoJson->insert(QStringLiteral("thumbnail_url"), url.toString()); + if (url().isValid()) + fillJson(infoJson, { "thumbnail_url"_ls, "thumbnail_file"_ls }, source); if (!imageSize.isEmpty()) - infoJson->insert(QStringLiteral("thumbnail_info"), - toInfoJson<ImageInfo>(*this)); + infoJson.insert(QStringLiteral("thumbnail_info"), + toInfoJson(*this)); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index 0d4c047e..af26c0a4 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -1,282 +1,254 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). +#include "filesourceinfo.h" +#include "quotient_export.h" + #include <QtCore/QJsonObject> +#include <QtCore/QMetaType> #include <QtCore/QMimeType> #include <QtCore/QSize> #include <QtCore/QUrl> -#include <QtCore/QMetaType> -namespace Quotient { -namespace EventContent { - /** - * A base class for all content types that can be stored - * in a RoomMessageEvent - * - * Each content type class should have a constructor taking - * a QJsonObject and override fillJson() with an implementation - * that will fill the target QJsonObject with stored values. It is - * assumed but not required that a content object can also be created - * from plain data. - */ - class Base { - public: - explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} - virtual ~Base() = default; - - // FIXME: make toJson() from converters.* work on base classes - QJsonObject toJson() const; - - public: - QJsonObject originalJson; - - protected: - Base(const Base&) = default; - Base(Base&&) = default; - - virtual void fillJson(QJsonObject* o) const = 0; - }; - - // The below structures fairly follow CS spec 11.2.1.6. The overall - // set of attributes for each content types is a superset of the spec - // but specific aggregation structure is altered. See doc comments to - // each type for the list of available attributes. - - // A quick classes inheritance structure follows: - // FileInfo - // FileContent : UrlBasedContent<FileInfo, Thumbnail> - // AudioContent : UrlBasedContent<FileInfo, Duration> - // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlBasedContent<ImageInfo, Thumbnail> - // VideoContent : UrlBasedContent<ImageInfo, Thumbnail, Duration> - - /** - * A base/mixin class for structures representing an "info" object for - * some content types. These include most attachment types currently in - * the CS API spec. - * - * In order to use it in a content class, derive both from TypedBase - * (or Base) and from FileInfo (or its derivative, such as \p ImageInfo) - * and call fillInfoJson() to fill the "info" subobject. Make sure - * to pass an "info" part of JSON to FileInfo constructor, not the whole - * JSON content, as well as contents of "url" (or a similar key) and - * optionally "filename" node from the main JSON content. Assuming you - * don't do unusual things, you should use \p UrlBasedContent<> instead - * of doing multiple inheritance and overriding Base::fillJson() by hand. - * - * This class is not polymorphic. - */ - class FileInfo { - public: - explicit FileInfo(const QUrl& u, qint64 payloadSize = -1, - const QMimeType& mimeType = {}, - const QString& originalFilename = {}); - FileInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - bool isValid() const; - - void fillInfoJson(QJsonObject* infoJson) const; - - /** - * \brief Extract media id from the URL - * - * This can be used, e.g., to construct a QML-facing image:// - * URI as follows: - * \code "image://provider/" + info.mediaId() \endcode - */ - QString mediaId() const { return url.authority() + url.path(); } - - public: - QJsonObject originalInfoJson; - QMimeType mimeType; - QUrl url; - qint64 payloadSize; - QString originalName; - }; - - template <typename InfoT> - QJsonObject toInfoJson(const InfoT& info) +class QFileInfo; + +namespace Quotient::EventContent { +//! \brief Base for all content types that can be stored in RoomMessageEvent +//! +//! Each content type class should have a constructor taking +//! a QJsonObject and override fillJson() with an implementation +//! that will fill the target QJsonObject with stored values. It is +//! assumed but not required that a content object can also be created +//! from plain data. +class QUOTIENT_API Base { +public: + explicit Base(QJsonObject o = {}) : originalJson(std::move(o)) {} + virtual ~Base() = default; + + QJsonObject toJson() const; + +public: + QJsonObject originalJson; + + // You can't assign those classes + Base& operator=(const Base&) = delete; + Base& operator=(Base&&) = delete; + +protected: + Base(const Base&) = default; + Base(Base&&) noexcept = default; + + virtual void fillJson(QJsonObject&) const = 0; +}; + +// The below structures fairly follow CS spec 11.2.1.6. The overall +// set of attributes for each content types is a superset of the spec +// but specific aggregation structure is altered. See doc comments to +// each type for the list of available attributes. + +// A quick classes inheritance structure follows (the definitions are +// spread across eventcontent.h and roommessageevent.h): +// UrlBasedContent<InfoT> : InfoT + thumbnail data +// PlayableContent<InfoT> : + duration attribute +// FileInfo +// FileContent = UrlBasedContent<FileInfo> +// AudioContent = PlayableContent<FileInfo> +// ImageInfo : FileInfo + imageSize attribute +// ImageContent = UrlBasedContent<ImageInfo> +// VideoContent = PlayableContent<ImageInfo> + +//! \brief Mix-in class representing `info` subobject in content JSON +//! +//! This is one of base classes for content types that deal with files or +//! URLs. It stores the file metadata attributes, such as size, MIME type +//! etc. found in the `content/info` subobject of event JSON payloads. +//! Actual content classes derive from this class _and_ TypedBase that +//! provides a polymorphic interface to access data in the mix-in. FileInfo +//! (as well as ImageInfo, that adds image size to the metadata) is NOT +//! polymorphic and is used in a non-polymorphic way to store thumbnail +//! metadata (in a separate instance), next to the metadata on the file +//! itself. +//! +//! If you need to make a new _content_ (not info) class based on files/URLs +//! take UrlBasedContent as the example, i.e.: +//! 1. Double-inherit from this class (or ImageInfo) and TypedBase. +//! 2. Provide a constructor from QJsonObject that will pass the `info` +//! subobject (not the whole content JSON) down to FileInfo/ImageInfo. +//! 3. Override fillJson() to customise the JSON export logic. Make sure +//! to call toInfoJson() from it to produce the payload for the `info` +//! subobject in the JSON payload. +//! +//! \sa ImageInfo, FileContent, ImageContent, AudioContent, VideoContent, +//! UrlBasedContent +class QUOTIENT_API FileInfo { +public: + FileInfo() = default; + //! \brief Construct from a QFileInfo object + //! + //! \param fi a QFileInfo object referring to an existing file + explicit FileInfo(const QFileInfo& fi); + explicit FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize = -1, + const QMimeType& mimeType = {}, + QString originalFilename = {}); + //! \brief Construct from a JSON `info` payload + //! + //! Make sure to pass the `info` subobject of content JSON, not the + //! whole JSON content. + FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + QString originalFilename = {}); + + bool isValid() const; + QUrl url() const; + + //! \brief Extract media id from the URL + //! + //! This can be used, e.g., to construct a QML-facing image:// + //! URI as follows: + //! \code "image://provider/" + info.mediaId() \endcode + QString mediaId() const { return url().authority() + url().path(); } + +public: + FileSourceInfo source; + QJsonObject originalInfoJson; + QMimeType mimeType; + qint64 payloadSize = 0; + QString originalName; +}; + +QUOTIENT_API QJsonObject toInfoJson(const FileInfo& info); + +//! \brief A content info class for image/video content types and thumbnails +class QUOTIENT_API ImageInfo : public FileInfo { +public: + ImageInfo() = default; + explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); + explicit ImageInfo(FileSourceInfo sourceInfo, qint64 fileSize = -1, + const QMimeType& type = {}, QSize imageSize = {}, + const QString& originalFilename = {}); + ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, + const QString& originalFilename = {}); + +public: + QSize imageSize; +}; + +QUOTIENT_API QJsonObject toInfoJson(const ImageInfo& info); + +//! \brief An auxiliary class for an info type that carries a thumbnail +//! +//! This class saves/loads a thumbnail to/from `info` subobject of +//! the JSON representation of event content; namely, `info/thumbnail_url` +//! (or, in case of an encrypted thumbnail, `info/thumbnail_file`) and +//! `info/thumbnail_info` fields are used. +class QUOTIENT_API Thumbnail : public ImageInfo { +public: + using ImageInfo::ImageInfo; + explicit Thumbnail(const QJsonObject& infoJson, + const Omittable<EncryptedFileMetadata>& efm = none); + + //! \brief Add thumbnail information to the passed `info` JSON object + void dumpTo(QJsonObject& infoJson) const; +}; + +class QUOTIENT_API TypedBase : public Base { +public: + 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; +}; + +//! \brief A template class for content types with a URL and additional info +//! +//! Types that derive from this class template take `url` (or, if the file +//! is encrypted, `file`) and, optionally, `filename` values from +//! the top-level JSON object and the rest of information from the `info` +//! subobject, as defined by the parameter type. +//! \tparam InfoT base info class - FileInfo or ImageInfo +template <class InfoT> +class UrlBasedContent : public TypedBase, public InfoT { +public: + using InfoT::InfoT; + explicit UrlBasedContent(const QJsonObject& json) + : TypedBase(json) + , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), + json["filename"].toString()) + , thumbnail(FileInfo::originalInfoJson) { - QJsonObject infoJson; - info.fillInfoJson(&infoJson); - return infoJson; + if (const auto efmJson = json.value("file"_ls).toObject(); + !efmJson.isEmpty()) + InfoT::source = fromJson<EncryptedFileMetadata>(efmJson); + // Two small hacks on originalJson to expose mediaIds to QML + originalJson.insert("mediaId", InfoT::mediaId()); + originalJson.insert("thumbnailMediaId", thumbnail.mediaId()); } - /** - * A content info class for image content types: image, thumbnail, video - */ - class ImageInfo : public FileInfo { - public: - explicit ImageInfo(const QUrl& u, qint64 fileSize = -1, - QMimeType mimeType = {}, const QSize& imageSize = {}, - const QString& originalFilename = {}); - ImageInfo(const QUrl& u, const QJsonObject& infoJson, - const QString& originalFilename = {}); - - void fillInfoJson(QJsonObject* infoJson) const; - - public: - QSize imageSize; - }; - - /** - * An auxiliary class for an info type that carries a thumbnail - * - * This class saves/loads a thumbnail to/from "info" subobject of - * the JSON representation of event content; namely, - * "info/thumbnail_url" and "info/thumbnail_info" fields are used. - */ - class Thumbnail : public ImageInfo { - public: - Thumbnail() : ImageInfo(QUrl()) {} // To allow empty thumbnails - Thumbnail(const QJsonObject& infoJson); - Thumbnail(const ImageInfo& info) : ImageInfo(info) {} - using ImageInfo::ImageInfo; - - /** - * Writes thumbnail information to "thumbnail_info" subobject - * and thumbnail URL to "thumbnail_url" node inside "info". - */ - void fillInfoJson(QJsonObject* infoJson) const; - }; - - class TypedBase : public Base { - public: - explicit TypedBase(QJsonObject o = {}) : Base(std::move(o)) {} - virtual QMimeType type() const = 0; - virtual const FileInfo* fileInfo() const { return nullptr; } - virtual FileInfo* fileInfo() { return nullptr; } - virtual const Thumbnail* thumbnailInfo() const { return nullptr; } - - protected: - using Base::Base; - }; - - /** - * A base class for content types that have a URL and additional info - * - * Types that derive from this class template take "url" and, - * optionally, "filename" values from the top-level JSON object and - * the rest of information from the "info" subobject, as defined by - * the parameter type. - * - * \tparam InfoT base info class - */ - template <class InfoT> - class UrlBasedContent : public TypedBase, public InfoT { - public: - using InfoT::InfoT; - explicit UrlBasedContent(const QJsonObject& json) - : TypedBase(json) - , InfoT(json["url"].toString(), json["info"].toObject(), - json["filename"].toString()) - { - // A small hack to facilitate links creation in QML. - originalJson.insert("mediaId", InfoT::mediaId()); - } - - QMimeType type() const override { return InfoT::mimeType; } - const FileInfo* fileInfo() const override { return this; } - FileInfo* fileInfo() override { return this; } - - protected: - void fillJson(QJsonObject* json) const override - { - Q_ASSERT(json); - json->insert("url", InfoT::url.toString()); - if (!InfoT::originalName.isEmpty()) - json->insert("filename", InfoT::originalName); - json->insert("info", toInfoJson<InfoT>(*this)); - } - }; - - template <typename InfoT> - class UrlWithThumbnailContent : public UrlBasedContent<InfoT> { - public: - // NB: when using inherited constructors, thumbnail has to be - // initialised separately - using UrlBasedContent<InfoT>::UrlBasedContent; - explicit UrlWithThumbnailContent(const QJsonObject& json) - : UrlBasedContent<InfoT>(json), thumbnail(InfoT::originalInfoJson) - { - // Another small hack, to simplify making a thumbnail link - UrlBasedContent<InfoT>::originalJson.insert("thumbnailMediaId", - thumbnail.mediaId()); - } - - const Thumbnail* thumbnailInfo() const override { return &thumbnail; } - - public: - Thumbnail thumbnail; - - protected: - void fillJson(QJsonObject* json) const override - { - UrlBasedContent<InfoT>::fillJson(json); - auto infoJson = json->take("info").toObject(); - thumbnail.fillInfoJson(&infoJson); - json->insert("info", infoJson); - } - }; - - /** - * Content class for m.image - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename (extension to the spec) - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - imageSize (QSize for a combination of "h" and "w" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: contents of - * thumbnail field, in the same vein as for the main image: - * - payloadSize - * - mimeType - * - imageSize - */ - using ImageContent = UrlWithThumbnailContent<ImageInfo>; - - /** - * Content class for m.file - * - * Available fields: - * - corresponding to the top-level JSON: - * - url - * - filename - * - corresponding to the "info" subobject: - * - payloadSize ("size" in JSON) - * - mimeType ("mimetype" in JSON) - * - thumbnail.url ("thumbnail_url" in JSON) - * - corresponding to the "info/thumbnail_info" subobject: - * - thumbnail.payloadSize - * - thumbnail.mimeType - * - thumbnail.imageSize (QSize for "h" and "w" in JSON) - */ - using FileContent = UrlWithThumbnailContent<FileInfo>; -} // namespace EventContent -} // namespace Quotient + QMimeType type() const override { return InfoT::mimeType; } + const FileInfo* fileInfo() const override { return this; } + FileInfo* fileInfo() override { return this; } + const Thumbnail* thumbnailInfo() const override { return &thumbnail; } + +public: + Thumbnail thumbnail; + +protected: + virtual void fillInfoJson(QJsonObject& infoJson [[maybe_unused]]) const + {} + + void fillJson(QJsonObject& json) const override + { + Quotient::fillJson(json, { "url"_ls, "file"_ls }, InfoT::source); + if (!InfoT::originalName.isEmpty()) + json.insert("filename", InfoT::originalName); + auto infoJson = toInfoJson(*this); + if (thumbnail.isValid()) + thumbnail.dumpTo(infoJson); + fillInfoJson(infoJson); + json.insert("info", infoJson); + } +}; + +//! \brief Content class for m.image +//! +//! Available fields: +//! - corresponding to the top-level JSON: +//! - source (corresponding to `url` or `file` in JSON) +//! - filename (extension to the spec) +//! - corresponding to the `info` subobject: +//! - payloadSize (`size` in JSON) +//! - mimeType (`mimetype` in JSON) +//! - imageSize (QSize for a combination of `h` and `w` in JSON) +//! - thumbnail.url (`thumbnail_url` in JSON) +//! - corresponding to the `info/thumbnail_info` subobject: contents of +//! thumbnail field, in the same vein as for the main image: +//! - payloadSize +//! - mimeType +//! - imageSize +using ImageContent = UrlBasedContent<ImageInfo>; + +//! \brief Content class for m.file +//! +//! Available fields: +//! - corresponding to the top-level JSON: +//! - source (corresponding to `url` or `file` in JSON) +//! - filename +//! - corresponding to the `info` subobject: +//! - payloadSize (`size` in JSON) +//! - mimeType (`mimetype` in JSON) +//! - thumbnail.source (`thumbnail_url` or `thumbnail_file` in JSON) +//! - corresponding to the `info/thumbnail_info` subobject: +//! - thumbnail.payloadSize +//! - thumbnail.mimeType +//! - thumbnail.imageSize (QSize for `h` and `w` in JSON) +using FileContent = UrlBasedContent<FileInfo>; +} // namespace Quotient::EventContent Q_DECLARE_METATYPE(const Quotient::EventContent::TypedBase*) diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index ebb96441..b4ac154c 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -1,86 +1,13 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #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 - * - * Use this factory template to detect the type from the JSON object - * contents (the detected event type should derive from the template - * parameter type) and create an event object of that type. - */ -template <typename BaseEventT> -inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) -{ - return _impl::loadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString()); -} - -/*! Create an event from a type string and content JSON - * - * Use this factory template to resolve the C++ type from the Matrix - * type string in \p matrixType and create an event of that type that has - * its content part set to \p content. - */ -template <typename BaseEventT> -inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, - const QJsonObject& content) -{ - return _impl::loadEvent<BaseEventT>(basicEventJson(matrixType, content), - matrixType); +struct [[deprecated( + "This header is obsolete since libQuotient 0.7; include a header with" + " the respective event type definition instead")]] EventLoaderH; +StateEventPtr eventLoaderH(EventLoaderH&); } - -/*! Create a state event from a type string, content JSON and state key - * - * Use this factory to resolve the C++ type from the Matrix type string - * in \p matrixType and create a state event of that type with content part - * set to \p content and state key set to \p stateKey (empty by default). - */ -inline StateEventPtr loadStateEvent(const QString& matrixType, - const QJsonObject& content, - const QString& stateKey = {}) -{ - return _impl::loadEvent<StateEventBase>( - basicStateEventJson(matrixType, content, stateKey), matrixType); -} - -template <typename EventT> -struct JsonConverter<event_ptr_tt<EventT>> { - static auto load(const QJsonValue& jv) - { - return loadEvent<EventT>(jv.toObject()); - } - static auto load(const QJsonDocument& jd) - { - return loadEvent<EventT>(jd.object()); - } -}; -} // namespace Quotient diff --git a/lib/events/eventrelation.cpp b/lib/events/eventrelation.cpp new file mode 100644 index 00000000..04972f45 --- /dev/null +++ b/lib/events/eventrelation.cpp @@ -0,0 +1,38 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "eventrelation.h" + +#include "../logging.h" +#include "event.h" + +using namespace Quotient; + +void JsonObjectConverter<EventRelation>::dumpTo(QJsonObject& jo, + const EventRelation& pod) +{ + if (pod.type.isEmpty()) { + qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; + return; + } + jo.insert(RelTypeKey, pod.type); + jo.insert(EventIdKey, pod.eventId); + if (pod.type == EventRelation::AnnotationType) + jo.insert(QStringLiteral("key"), pod.key); +} + +void JsonObjectConverter<EventRelation>::fillFrom(const QJsonObject& jo, + EventRelation& pod) +{ + if (const auto replyJson = jo.value(EventRelation::ReplyType).toObject(); + !replyJson.isEmpty()) { + pod.type = EventRelation::ReplyType; + fromJson(replyJson[EventIdKeyL], pod.eventId); + } else { + // The experimental logic for generic relationships (MSC1849) + fromJson(jo[RelTypeKey], pod.type); + fromJson(jo[EventIdKeyL], pod.eventId); + if (pod.type == EventRelation::AnnotationType) + fromJson(jo["key"_ls], pod.key); + } +} diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h new file mode 100644 index 00000000..2a841cf1 --- /dev/null +++ b/lib/events/eventrelation.h @@ -0,0 +1,52 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +namespace Quotient { + +[[maybe_unused]] constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto RelTypeKey = "rel_type"_ls; + +struct QUOTIENT_API EventRelation { + using reltypeid_t = QLatin1String; + + QString type; + QString eventId; + QString key = {}; // Only used for m.annotation for now + + static constexpr auto ReplyType = "m.in_reply_to"_ls; + static constexpr auto AnnotationType = "m.annotation"_ls; + static constexpr auto ReplacementType = "m.replace"_ls; + + static EventRelation replyTo(QString eventId) + { + return { ReplyType, std::move(eventId) }; + } + static EventRelation annotate(QString eventId, QString key) + { + return { AnnotationType, std::move(eventId), std::move(key) }; + } + static EventRelation replace(QString eventId) + { + return { ReplacementType, std::move(eventId) }; + } + + [[deprecated("Use ReplyType variable instead")]] + static constexpr auto Reply() { return ReplyType; } + [[deprecated("Use AnnotationType variable instead")]] // + static constexpr auto Annotation() { return AnnotationType; } + [[deprecated("Use ReplacementType variable instead")]] // + static constexpr auto Replacement() { return ReplacementType; } +}; + +template <> +struct QUOTIENT_API JsonObjectConverter<EventRelation> { + static void dumpTo(QJsonObject& jo, const EventRelation& pod); + static void fillFrom(const QJsonObject& jo, EventRelation& pod); +}; + +} + diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp new file mode 100644 index 00000000..a60d86d2 --- /dev/null +++ b/lib/events/filesourceinfo.cpp @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "filesourceinfo.h" + +#include "logging.h" +#include "util.h" + +#ifdef Quotient_E2EE_ENABLED +# include "e2ee/qolmutils.h" + +# include <QtCore/QCryptographicHash> + +# include <openssl/evp.h> +#endif + +using namespace Quotient; + +QByteArray Quotient::decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata) +{ +#ifdef Quotient_E2EE_ENABLED + if (QByteArray::fromBase64(metadata.hashes["sha256"_ls].toLatin1()) + != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { + qCWarning(E2EE) << "Hash verification failed for file"; + return {}; + } + + auto _key = metadata.key.k; + const auto keyBytes = QByteArray::fromBase64( + _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + QByteArray plaintext(ciphertext.size() + EVP_MAX_BLOCK_LENGTH - 1, '\0'); + EVP_DecryptInit_ex( + ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast<const unsigned char*>(keyBytes.data()), + reinterpret_cast<const unsigned char*>( + QByteArray::fromBase64(metadata.iv.toLatin1()).data())); + EVP_DecryptUpdate(ctx, reinterpret_cast<unsigned char*>(plaintext.data()), + &length, + reinterpret_cast<const unsigned char*>(ciphertext.data()), + ciphertext.size()); + EVP_DecryptFinal_ex(ctx, + reinterpret_cast<unsigned char*>(plaintext.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + return plaintext.left(ciphertext.size()); +#else + qWarning(MAIN) << "This build of libQuotient doesn't support E2EE, " + "cannot decrypt the file"; + return ciphertext; +#endif +} + +std::pair<EncryptedFileMetadata, QByteArray> Quotient::encryptFile( + const QByteArray& plainText) +{ +#ifdef Quotient_E2EE_ENABLED + auto k = RandomBuffer(32); + auto kBase64 = k.toBase64(QByteArray::Base64UrlEncoding + | QByteArray::OmitTrailingEquals); + auto iv = RandomBuffer(16); + JWK key = { + "oct"_ls, { "encrypt"_ls, "decrypt"_ls }, "A256CTR"_ls, kBase64, true + }; + + int length = -1; + auto* ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, k.bytes(), iv.bytes()); + const auto blockSize = EVP_CIPHER_CTX_block_size(ctx); + QByteArray cipherText(plainText.size() + blockSize - 1, '\0'); + EVP_EncryptUpdate(ctx, reinterpret_cast<unsigned char*>(cipherText.data()), + &length, + reinterpret_cast<const unsigned char*>(plainText.data()), + plainText.size()); + EVP_EncryptFinal_ex(ctx, + reinterpret_cast<unsigned char*>(cipherText.data()) + + length, + &length); + EVP_CIPHER_CTX_free(ctx); + + auto hash = QCryptographicHash::hash(cipherText, QCryptographicHash::Sha256) + .toBase64(QByteArray::OmitTrailingEquals); + auto ivBase64 = iv.toBase64(QByteArray::OmitTrailingEquals); + EncryptedFileMetadata efm = { + {}, key, ivBase64, { { QStringLiteral("sha256"), hash } }, "v2"_ls + }; + return { efm, cipherText }; +#else + return {}; +#endif +} + +void JsonObjectConverter<EncryptedFileMetadata>::dumpTo(QJsonObject& jo, + const EncryptedFileMetadata& 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); +} + +void JsonObjectConverter<EncryptedFileMetadata>::fillFrom(const QJsonObject& jo, + EncryptedFileMetadata& 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); +} + +void JsonObjectConverter<JWK>::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); +} + +void JsonObjectConverter<JWK>::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); +} + +QUrl Quotient::getUrlFromSourceInfo(const FileSourceInfo& fsi) +{ + return std::visit(Overloads { [](const QUrl& url) { return url; }, + [](const EncryptedFileMetadata& efm) { + return efm.url; + } }, + fsi); +} + +void Quotient::setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl) +{ + std::visit(Overloads { [&newUrl](QUrl& url) { url = newUrl; }, + [&newUrl](EncryptedFileMetadata& efm) { + efm.url = newUrl; + } }, + fsi); +} + +void Quotient::fillJson(QJsonObject& jo, + const std::array<QLatin1String, 2>& jsonKeys, + const FileSourceInfo& fsi) +{ + // NB: Keeping variant_size_v out of the function signature for readability. + // NB2: Can't use jsonKeys directly inside static_assert as its value is + // unknown so the compiler cannot ensure size() is constexpr (go figure...) + static_assert( + std::variant_size_v<FileSourceInfo> == decltype(jsonKeys) {}.size()); + jo.insert(jsonKeys[fsi.index()], toJson(fsi)); +} diff --git a/lib/events/filesourceinfo.h b/lib/events/filesourceinfo.h new file mode 100644 index 00000000..8f7e3cbe --- /dev/null +++ b/lib/events/filesourceinfo.h @@ -0,0 +1,90 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "converters.h" + +#include <array> + +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 QUOTIENT_API EncryptedFileMetadata { + 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; +}; + +QUOTIENT_API std::pair<EncryptedFileMetadata, QByteArray> encryptFile( + const QByteArray& plainText); +QUOTIENT_API QByteArray decryptFile(const QByteArray& ciphertext, + const EncryptedFileMetadata& metadata); + +template <> +struct QUOTIENT_API JsonObjectConverter<EncryptedFileMetadata> { + static void dumpTo(QJsonObject& jo, const EncryptedFileMetadata& pod); + static void fillFrom(const QJsonObject& jo, EncryptedFileMetadata& pod); +}; + +template <> +struct QUOTIENT_API JsonObjectConverter<JWK> { + static void dumpTo(QJsonObject& jo, const JWK& pod); + static void fillFrom(const QJsonObject& jo, JWK& pod); +}; + +using FileSourceInfo = std::variant<QUrl, EncryptedFileMetadata>; + +QUOTIENT_API QUrl getUrlFromSourceInfo(const FileSourceInfo& fsi); + +QUOTIENT_API void setUrlInSourceInfo(FileSourceInfo& fsi, const QUrl& newUrl); + +// The way FileSourceInfo is stored in JSON requires an extra parameter so +// the original template is not applicable +template <> +void fillJson(QJsonObject&, const FileSourceInfo&) = delete; + +//! \brief Export FileSourceInfo to a JSON object +//! +//! Depending on what is stored inside FileSourceInfo, this function will insert +//! - a key-to-string pair where key is taken from jsonKeys[0] and the string +//! is the URL, if FileSourceInfo stores a QUrl; +//! - a key-to-object mapping where key is taken from jsonKeys[1] and the object +//! is the result of converting EncryptedFileMetadata to JSON, +//! if FileSourceInfo stores EncryptedFileMetadata +QUOTIENT_API void fillJson(QJsonObject& jo, + const std::array<QLatin1String, 2>& jsonKeys, + const FileSourceInfo& fsi); + +} // namespace Quotient diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h new file mode 100644 index 00000000..80aebcf3 --- /dev/null +++ b/lib/events/keyverificationevent.h @@ -0,0 +1,258 @@ +// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "event.h" + +namespace Quotient { + +static constexpr auto SasV1Method = "m.sas.v1"_ls; + +class QUOTIENT_API KeyVerificationEvent : public Event { +public: + QUO_BASE_EVENT(KeyVerificationEvent, "m.key.*"_ls, Event::BaseMetaType) + + using Event::Event; + + /// An opaque identifier for the verification request. Must + /// be unique with respect to the devices involved. + QUO_CONTENT_GETTER(QString, transactionId) +}; + +/// Requests a key verification with another user's devices. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationRequestEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationRequestEvent, "m.key.verification.request") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationRequestEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods, + const QDateTime& timestamp) + : KeyVerificationRequestEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) }, + { "timestamp"_ls, toJson(timestamp) } })) + {} + + /// The device ID which is initiating the request. + QUO_CONTENT_GETTER(QString, fromDevice) + + /// The verification methods supported by the sender. + QUO_CONTENT_GETTER(QStringList, methods) + + /// The POSIX timestamp in milliseconds for when the request was + /// made. If the request is in the future by more than 5 minutes or + /// more than 10 minutes in the past, the message should be ignored + /// by the receiver. + QUO_CONTENT_GETTER(QDateTime, timestamp) +}; + +class QUOTIENT_API KeyVerificationReadyEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationReadyEvent, "m.key.verification.ready") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationReadyEvent(const QString& transactionId, + const QString& fromDevice, + const QStringList& methods) + : KeyVerificationReadyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "methods"_ls, toJson(methods) } })) + {} + + /// The device ID which is accepting the request. + QUO_CONTENT_GETTER(QString, fromDevice) + + /// The verification methods supported by the sender. + QUO_CONTENT_GETTER(QStringList, methods) +}; + +/// Begins a key verification process. +class QUOTIENT_API KeyVerificationStartEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationStartEvent, "m.key.verification.start") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationStartEvent(const QString& transactionId, + const QString& fromDevice) + : KeyVerificationStartEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "from_device"_ls, fromDevice }, + { "method"_ls, SasV1Method }, + { "hashes"_ls, QJsonArray{ "sha256"_ls } }, + { "key_agreement_protocols"_ls, + QJsonArray{ "curve25519-hkdf-sha256"_ls } }, + { "message_authentication_codes"_ls, + QJsonArray{ "hkdf-hmac-sha256"_ls } }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls } } })) + {} + + /// The device ID which is initiating the process. + QUO_CONTENT_GETTER(QString, fromDevice) + + /// The verification method to use. + QUO_CONTENT_GETTER(QString, method) + + /// Optional method to use to verify the other user's key with. + QUO_CONTENT_GETTER(Omittable<QString>, nextMethod) + + // SAS.V1 methods + + /// The key agreement protocols the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList keyAgreementProtocols() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QStringList>("key_agreement_protocols"_ls); + } + + /// The hash methods the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList hashes() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QStringList>("hashes"_ls); + } + + /// The message authentication codes that the sending device understands. + /// \note Only exist if method is m.sas.v1 + QStringList messageAuthenticationCodes() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QStringList>("message_authentication_codes"_ls); + } + + /// The SAS methods the sending device (and the sending device's + /// user) understands. + /// \note Only exist if method is m.sas.v1 + QString shortAuthenticationString() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QString>("short_authentification_string"_ls); + } +}; + +/// Accepts a previously sent m.key.verification.start message. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationAcceptEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationAcceptEvent, "m.key.verification.accept") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationAcceptEvent(const QString& transactionId, + const QString& commitment) + : KeyVerificationAcceptEvent(basicJson( + TypeId, { { "transaction_id"_ls, transactionId }, + { "method"_ls, SasV1Method }, + { "key_agreement_protocol"_ls, "curve25519-hkdf-sha256" }, + { "hash"_ls, "sha256" }, + { "message_authentication_code"_ls, "hkdf-hmac-sha256" }, + { "short_authentication_string"_ls, + QJsonArray{ "decimal"_ls, "emoji"_ls, } }, + { "commitment"_ls, commitment } })) + {} + + /// The verification method to use. Must be 'm.sas.v1'. + QUO_CONTENT_GETTER(QString, method) + + /// The key agreement protocol the device is choosing to use, out of + /// the options in the m.key.verification.start message. + QUO_CONTENT_GETTER(QString, keyAgreementProtocol) + + /// The hash method the device is choosing to use, out of the + /// options in the m.key.verification.start message. + QUO_CONTENT_GETTER_X(QString, hashData, "hash"_ls) + + /// The message authentication code the device is choosing to use, out + /// of the options in the m.key.verification.start message. + QUO_CONTENT_GETTER(QString, messageAuthenticationCode) + + /// The SAS methods both devices involved in the verification process understand. + QUO_CONTENT_GETTER(QStringList, shortAuthenticationString) + + /// The hash (encoded as unpadded base64) of the concatenation of the + /// device's ephemeral public key (encoded as unpadded base64) and the + /// canonical JSON representation of the m.key.verification.start message. + QUO_CONTENT_GETTER(QString, commitment) +}; + +class QUOTIENT_API KeyVerificationCancelEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationCancelEvent, "m.key.verification.cancel") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationCancelEvent(const QString& transactionId, + const QString& reason) + : KeyVerificationCancelEvent( + basicJson(TypeId, { + { "transaction_id"_ls, transactionId }, + { "reason"_ls, reason }, + { "code"_ls, reason } // Not a typo + })) + {} + + /// A human readable description of the code. The client should only + /// rely on this string if it does not understand the code. + QUO_CONTENT_GETTER(QString, reason) + + /// The error code for why the process/request was cancelled by the user. + QUO_CONTENT_GETTER(QString, code) +}; + +/// Sends the ephemeral public key for a device to the partner device. +/// Typically sent as a to-device event. +class QUOTIENT_API KeyVerificationKeyEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationKeyEvent, "m.key.verification.key") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationKeyEvent(const QString& transactionId, const QString& key) + : KeyVerificationKeyEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "key"_ls, key } })) + {} + + /// The device's ephemeral public key, encoded as unpadded base64. + QUO_CONTENT_GETTER(QString, key) +}; + +/// Sends the MAC of a device's key to the partner device. +class QUOTIENT_API KeyVerificationMacEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationMacEvent, "m.key.verification.mac") + + using KeyVerificationEvent::KeyVerificationEvent; + KeyVerificationMacEvent(const QString& transactionId, const QString& keys, + const QJsonObject& mac) + : KeyVerificationMacEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId }, + { "keys"_ls, keys }, + { "mac"_ls, mac } })) + {} + + /// The device's ephemeral public key, encoded as unpadded base64. + QUO_CONTENT_GETTER(QString, keys) + + QHash<QString, QString> mac() const + { + return contentPart<QHash<QString, QString>>("mac"_ls); + } +}; + +class QUOTIENT_API KeyVerificationDoneEvent : public KeyVerificationEvent { +public: + QUO_EVENT(KeyVerificationDoneEvent, "m.key.verification.done") + + using KeyVerificationEvent::KeyVerificationEvent; + explicit KeyVerificationDoneEvent(const QString& transactionId) + : KeyVerificationDoneEvent( + basicJson(TypeId, { { "transaction_id"_ls, transactionId } })) + {} +}; +} // namespace Quotient diff --git a/lib/events/reactionevent.cpp b/lib/events/reactionevent.cpp deleted file mode 100644 index 003c8ead..00000000 --- a/lib/events/reactionevent.cpp +++ /dev/null @@ -1,44 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "reactionevent.h" - -using namespace Quotient; - -void JsonObjectConverter<EventRelation>::dumpTo( - QJsonObject& jo, const EventRelation& pod) -{ - if (pod.type.isEmpty()) { - qCWarning(MAIN) << "Empty relation type; won't dump to JSON"; - return; - } - jo.insert(QStringLiteral("rel_type"), pod.type); - jo.insert(EventIdKey, pod.eventId); - if (pod.type == EventRelation::Annotation()) - jo.insert(QStringLiteral("key"), pod.key); -} - -void JsonObjectConverter<EventRelation>::fillFrom( - const QJsonObject& jo, EventRelation& pod) -{ - // The experimental logic for generic relationships (MSC1849) - fromJson(jo["rel_type"_ls], pod.type); - fromJson(jo[EventIdKeyL], pod.eventId); - if (pod.type == EventRelation::Annotation()) - fromJson(jo["key"_ls], pod.key); -} diff --git a/lib/events/reactionevent.h b/lib/events/reactionevent.h index 75c6528c..8d873441 100644 --- a/lib/events/reactionevent.h +++ b/lib/events/reactionevent.h @@ -1,73 +1,14 @@ -/****************************************************************************** - * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "roomevent.h" +#include "eventrelation.h" namespace Quotient { -struct EventRelation { - using reltypeid_t = const char*; - static constexpr reltypeid_t Reply() { return "m.in_reply_to"; } - static constexpr reltypeid_t Annotation() { return "m.annotation"; } - static constexpr reltypeid_t Replacement() { return "m.replace"; } - - QString type; - QString eventId; - QString key = {}; // Only used for m.annotation for now - - static EventRelation replyTo(QString eventId) - { - return { Reply(), std::move(eventId) }; - } - static EventRelation annotate(QString eventId, QString key) - { - return { Annotation(), std::move(eventId), std::move(key) }; - } - static EventRelation replace(QString eventId) - { - return { Replacement(), std::move(eventId) }; - } -}; -template <> -struct JsonObjectConverter<EventRelation> { - static void dumpTo(QJsonObject& jo, const EventRelation& pod); - static void fillFrom(const QJsonObject& jo, EventRelation& pod); -}; - -class ReactionEvent : public RoomEvent { -public: - DEFINE_EVENT_TYPEID("m.reaction", ReactionEvent) - - explicit ReactionEvent(const EventRelation& value) - : RoomEvent(typeId(), matrixTypeId(), - { { QStringLiteral("m.relates_to"), toJson(value) } }) - {} - explicit ReactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) {} - EventRelation relation() const - { - return content<EventRelation>(QStringLiteral("m.relates_to")); - } - -private: - EventRelation _relation; -}; -REGISTER_EVENT_TYPE(ReactionEvent) +DEFINE_SIMPLE_EVENT(ReactionEvent, RoomEvent, "m.reaction", EventRelation, + relation, "m.relates_to") } // namespace Quotient diff --git a/lib/events/receiptevent.cpp b/lib/events/receiptevent.cpp index bf050cb2..d8f9fa0b 100644 --- a/lib/events/receiptevent.cpp +++ b/lib/events/receiptevent.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later /* Example of a Receipt Event: @@ -35,31 +20,49 @@ Example of a Receipt Event: #include "receiptevent.h" -#include "converters.h" #include "logging.h" using namespace Quotient; -ReceiptEvent::ReceiptEvent(const QJsonObject& obj) : Event(typeId(), obj) +// 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 Quotient::toJson(const EventsWithReceipts& ewrs) { - const auto& contents = contentJson(); - _eventsWithReceipts.reserve(contents.size()); - for (auto eventIt = contents.begin(); eventIt != contents.end(); ++eventIt) { + 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; +} + +template<> +EventsWithReceipts Quotient::fromJson(const QJsonObject& json) +{ + EventsWithReceipts result; + result.reserve(json.size()); + for (auto eventIt = json.begin(); eventIt != json.end(); ++eventIt) { if (eventIt.key().isEmpty()) { qCWarning(EPHEMERAL) << "ReceiptEvent has an empty event id, skipping"; - qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << contents; + qCDebug(EPHEMERAL) << "ReceiptEvent content follows:\n" << json; continue; } - const QJsonObject reads = + 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 QJsonObject user = userIt.value().toObject(); - receipts.push_back( + const auto user = userIt.value().toObject(); + usersAtEvent.push_back( { userIt.key(), fromJson<QDateTime>(user["ts"_ls]) }); } - _eventsWithReceipts.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 dd54a476..b87e00f6 100644 --- a/lib/events/receiptevent.h +++ b/lib/events/receiptevent.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -24,28 +9,27 @@ #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 QJsonObject& obj); +template <> +QUOTIENT_API EventsWithReceipts fromJson(const QJsonObject& json); +QUOTIENT_API QJsonObject toJson(const EventsWithReceipts& ewrs); - const EventsWithReceipts& eventsWithReceipts() const - { - return _eventsWithReceipts; - } +class QUOTIENT_API ReceiptEvent + : public EventTemplate<ReceiptEvent, Event, EventsWithReceipts> { +public: + QUO_EVENT(ReceiptEvent, "m.receipt") + using EventTemplate::EventTemplate; -private: - EventsWithReceipts _eventsWithReceipts; + [[deprecated("Use content() instead")]] + EventsWithReceipts eventsWithReceipts() const { return content(); } }; -REGISTER_EVENT_TYPE(ReceiptEvent) } // namespace Quotient diff --git a/lib/events/redactionevent.cpp b/lib/events/redactionevent.cpp deleted file mode 100644 index bf467718..00000000 --- a/lib/events/redactionevent.cpp +++ /dev/null @@ -1 +0,0 @@ -#include "redactionevent.h" diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index 3b3af18e..a2e0b73b 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -1,38 +1,21 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "roomevent.h" namespace Quotient { -class RedactionEvent : public RoomEvent { +class QUOTIENT_API RedactionEvent : public RoomEvent { public: - DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) + QUO_EVENT(RedactionEvent, "m.room.redaction") - explicit RedactionEvent(const QJsonObject& obj) : RoomEvent(typeId(), obj) - {} + using RoomEvent::RoomEvent; QString redactedEvent() const { return fullJson()["redacts"_ls].toString(); } - QString reason() const { return contentJson()["reason"_ls].toString(); } + QUO_CONTENT_GETTER(QString, reason) }; -REGISTER_EVENT_TYPE(RedactionEvent) } // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index c2100eaa..1986f852 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -22,28 +7,17 @@ #include "stateevent.h" namespace Quotient { -class RoomAvatarEvent : public StateEvent<EventContent::ImageContent> { +class QUOTIENT_API RoomAvatarEvent + : public KeylessStateEventBase<RoomAvatarEvent, + EventContent::ImageContent> { // It's a bit of an overkill to use a full-fledged ImageContent // because in reality m.room.avatar usually only has a single URL, // without a thumbnail. But The Spec says there be thumbnails, and - // we follow The Spec. + // we follow The Spec (and ImageContent is very convenient to reuse here). public: - DEFINE_EVENT_TYPEID("m.room.avatar", RoomAvatarEvent) - explicit RoomAvatarEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) - {} - explicit RoomAvatarEvent(const EventContent::ImageContent& avatar) - : StateEvent(typeId(), matrixTypeId(), QString(), avatar) - {} - // A replica of EventContent::ImageInfo constructor - explicit RoomAvatarEvent(const QUrl& u, qint64 fileSize = -1, - QMimeType mimeType = {}, - const QSize& imageSize = {}, - const QString& originalFilename = {}) - : RoomAvatarEvent(EventContent::ImageContent { - u, fileSize, mimeType, imageSize, originalFilename }) - {} + QUO_EVENT(RoomAvatarEvent, "m.room.avatar") + using KeylessStateEventBase::KeylessStateEventBase; - QUrl url() const { return content().url; } + QUrl url() const { return content().url(); } }; -REGISTER_EVENT_TYPE(RoomAvatarEvent) } // namespace Quotient diff --git a/lib/events/roomcanonicalaliasevent.h b/lib/events/roomcanonicalaliasevent.h index fadfece0..c73bc92a 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -1,78 +1,44 @@ -/****************************************************************************** - * Copyright (C) 2020 QMatrixClient project - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com> +// SPDX-FileCopyrightText: 2020 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "stateevent.h" namespace Quotient { -namespace EventContent{ - class AliasesEventContent { - - public: - - template<typename T1, typename T2> - AliasesEventContent(T1&& canonicalAlias, T2&& altAliases) - : canonicalAlias(std::forward<T1>(canonicalAlias)) - , altAliases(std::forward<T2>(altAliases)) - { } - - AliasesEventContent(const QJsonObject& json) - : canonicalAlias(fromJson<QString>(json["alias"])) - , altAliases(fromJson<QStringList>(json["alt_aliases"])) - { } - - auto toJson() const - { - QJsonObject jo; - addParam<IfNotEmpty>(jo, QStringLiteral("alias"), canonicalAlias); - addParam<IfNotEmpty>(jo, QStringLiteral("alt_aliases"), altAliases); - return jo; - } - +namespace EventContent { + struct AliasesEventContent { QString canonicalAlias; QStringList altAliases; }; } // namespace EventContent -class RoomCanonicalAliasEvent - : public StateEvent<EventContent::AliasesEventContent> { +template<> +inline EventContent::AliasesEventContent fromJson(const QJsonObject& jo) +{ + return EventContent::AliasesEventContent { + fromJson<QString>(jo["alias"_ls]), + fromJson<QStringList>(jo["alt_aliases"_ls]) + }; +} +template<> +inline auto toJson(const EventContent::AliasesEventContent& c) +{ + QJsonObject jo; + addParam<IfNotEmpty>(jo, QStringLiteral("alias"), c.canonicalAlias); + addParam<IfNotEmpty>(jo, QStringLiteral("alt_aliases"), c.altAliases); + return jo; +} + +class QUOTIENT_API RoomCanonicalAliasEvent + : public KeylessStateEventBase<RoomCanonicalAliasEvent, + EventContent::AliasesEventContent> { public: - DEFINE_EVENT_TYPEID("m.room.canonical_alias", RoomCanonicalAliasEvent) - - explicit RoomCanonicalAliasEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - { } - - explicit RoomCanonicalAliasEvent(const QString& canonicalAlias, - const QStringList& altAliases = {}) - : StateEvent(typeId(), matrixTypeId(), QString(), - canonicalAlias, altAliases) - { } - - explicit RoomCanonicalAliasEvent(QString&& canonicalAlias, - QStringList&& altAliases = {}) - : StateEvent(typeId(), matrixTypeId(), QString(), - std::move(canonicalAlias), std::move(altAliases)) - { } + QUO_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias") + using KeylessStateEventBase::KeylessStateEventBase; QString alias() const { return content().canonicalAlias; } QStringList altAliases() const { return content().altAliases; } }; -REGISTER_EVENT_TYPE(RoomCanonicalAliasEvent) } // namespace Quotient diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index c72b5bc2..3b5024d5 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -1,43 +1,40 @@ -/****************************************************************************** - * Copyright (C) 2019 QMatrixClient project - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roomcreateevent.h" using namespace Quotient; +template <> +RoomType Quotient::fromJson(const QJsonValue& jv) +{ + return enumFromJsonString(jv.toString(), RoomTypeStrings, + 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(); - return { fromJson<QString>(predJson["room_id"_ls]), - fromJson<QString>(predJson["event_id"_ls]) }; + const auto predJson = contentPart<QJsonObject>("predecessor"_ls); + return { fromJson<QString>(predJson[RoomIdKeyL]), + fromJson<QString>(predJson[EventIdKeyL]) }; } 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 91aefe9e..5968e187 100644 --- a/lib/events/roomcreateevent.h +++ b/lib/events/roomcreateevent.h @@ -1,34 +1,17 @@ -/****************************************************************************** - * Copyright (C) 2019 QMatrixClient project - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { -class RoomCreateEvent : public StateEventBase { +class QUOTIENT_API RoomCreateEvent : public StateEvent { public: - DEFINE_EVENT_TYPEID("m.room.create", RoomCreateEvent) + QUO_EVENT(RoomCreateEvent, "m.room.create") - explicit RoomCreateEvent() : StateEventBase(typeId(), matrixTypeId()) {} - explicit RoomCreateEvent(const QJsonObject& obj) - : StateEventBase(typeId(), obj) - {} + using StateEvent::StateEvent; struct Predecessor { QString roomId; @@ -39,6 +22,6 @@ 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 a59cd6e0..e98cb591 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -1,43 +1,18 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roomevent.h" -#include "converters.h" #include "logging.h" #include "redactionevent.h" 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) -{} - -RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) +RoomEvent::RoomEvent(const QJsonObject& json) : Event(json) { - const auto unsignedData = json[UnsignedKeyL].toObject(); - const auto redaction = unsignedData[RedactedCauseKeyL]; - if (redaction.isObject()) - _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); + if (const auto redaction = unsignedPart<QJsonObject>(RedactedCauseKeyL); + !redaction.isEmpty()) + _redactedBecause = loadEvent<RedactionEvent>(redaction); } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job @@ -51,24 +26,24 @@ QDateTime RoomEvent::originTimestamp() const QString RoomEvent::roomId() const { - return fullJson()["room_id"_ls].toString(); + return fullJson()[RoomIdKeyL].toString(); } QString RoomEvent::senderId() const { - return fullJson()["sender"_ls].toString(); + return fullJson()[SenderKeyL].toString(); } 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 } @@ -80,7 +55,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 @@ -90,12 +65,12 @@ QString RoomEvent::stateKey() const void RoomEvent::setRoomId(const QString& roomId) { - editJson().insert(QStringLiteral("room_id"), roomId); + editJson().insert(RoomIdKey, roomId); } void RoomEvent::setSender(const QString& senderId) { - editJson().insert(QStringLiteral("sender"), senderId); + editJson().insert(SenderKey, senderId); } void RoomEvent::setTransactionId(const QString& txnId) @@ -115,24 +90,23 @@ void RoomEvent::addId(const QString& newId) Q_ASSERT(id() == newId); } -QJsonObject makeCallContentJson(const QString& callId, int version, - QJsonObject content) +void RoomEvent::dumpTo(QDebug dbg) const { - content.insert(QStringLiteral("call_id"), callId); - content.insert(QStringLiteral("version"), version); - return content; + Event::dumpTo(dbg); + dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')'; } -CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, - const QString& callId, int version, - const QJsonObject& contentJson) - : RoomEvent(type, matrixType, - makeCallContentJson(callId, version, contentJson)) -{} +#ifdef Quotient_E2EE_ENABLED +void RoomEvent::setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent) +{ + _originalEvent = std::move(originalEvent); +} -CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) - : RoomEvent(type, json) +const QJsonObject RoomEvent::encryptedJson() const { - if (callId().isEmpty()) - qCWarning(EVENTS) << id() << "is a call event with an empty call id"; + if(!_originalEvent) { + return {}; + } + return _originalEvent->fullJson(); } +#endif diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 621652cb..203434f6 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -25,33 +10,22 @@ namespace Quotient { class RedactionEvent; -/** This class corresponds to m.room.* events */ -class RoomEvent : public Event { - Q_GADGET - Q_PROPERTY(QString id READ id) - Q_PROPERTY(QDateTime timestamp READ timestamp CONSTANT) - Q_PROPERTY(QString roomId READ roomId CONSTANT) - Q_PROPERTY(QString senderId READ senderId CONSTANT) - Q_PROPERTY(QString redactionReason READ redactionReason) - Q_PROPERTY(bool isRedacted READ isRedacted) - Q_PROPERTY(QString transactionId READ transactionId WRITE setTransactionId) +// That check could look into Event and find most stuff already deleted... +// NOLINTNEXTLINE(cppcoreguidelines-special-member-functions) +class QUOTIENT_API RoomEvent : public Event { public: - using factory_t = EventFactory<RoomEvent>; + QUO_BASE_EVENT(RoomEvent, {}, Event::BaseMetaType) - // RedactionEvent is an incomplete type here so we cannot inline - // constructors and destructors and we cannot use 'using'. - RoomEvent(Type type, event_mtype_t matrixType, - const QJsonObject& contentJson = {}); - RoomEvent(Type type, const QJsonObject& json); - ~RoomEvent() override; + ~RoomEvent() override; // Don't inline this - see the private section 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); } @@ -63,48 +37,48 @@ 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); +#ifdef Quotient_E2EE_ENABLED + void setOriginalEvent(event_ptr_tt<RoomEvent>&& originalEvent); + const RoomEvent* originalEvent() const { return _originalEvent.get(); } + const QJsonObject encryptedJson() const; +#endif + +protected: + explicit RoomEvent(const QJsonObject& json); + void dumpTo(QDebug dbg) const override; + private: + // RedactionEvent is an incomplete type here so we cannot inline + // constructors using it and also destructors (with 'using', in particular). event_ptr_tt<RedactionEvent> _redactedBecause; + +#ifdef Quotient_E2EE_ENABLED + event_ptr_tt<RoomEvent> _originalEvent; +#endif }; using RoomEventPtr = event_ptr_tt<RoomEvent>; using RoomEvents = EventsArray<RoomEvent>; using RoomEventsRange = Range<RoomEvents>; -class CallEventBase : public RoomEvent { -public: - CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, - int version, const QJsonObject& contentJson = {}); - CallEventBase(Type type, const QJsonObject& json); - ~CallEventBase() override = default; - bool isCallEvent() const override { return true; } - - QString callId() const { return content<QString>("call_id"_ls); } - int version() const { return content<int>("version"_ls); } -}; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) Q_DECLARE_METATYPE(const Quotient::RoomEvent*) diff --git a/lib/events/roomkeyevent.cpp b/lib/events/roomkeyevent.cpp deleted file mode 100644 index 66580430..00000000 --- a/lib/events/roomkeyevent.cpp +++ /dev/null @@ -1,9 +0,0 @@ -#include "roomkeyevent.h" - -using namespace Quotient; - -RoomKeyEvent::RoomKeyEvent(const QJsonObject &obj) : Event(typeId(), obj) -{ - if (roomId().isEmpty()) - qCWarning(E2EE) << "Room key event has empty room id"; -} diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index 679cbf7c..dad5df8b 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -1,19 +1,33 @@ +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "event.h" namespace Quotient { -class RoomKeyEvent : public Event +class QUOTIENT_API RoomKeyEvent : public Event { public: - DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) + QUO_EVENT(RoomKeyEvent, "m.room_key") - RoomKeyEvent(const QJsonObject& obj); + using Event::Event; + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, + const QString& sessionId, const QString& sessionKey) + : Event(basicJson(TypeId, { + { "algorithm", algorithm }, + { "room_id", roomId }, + { "session_id", sessionId }, + { "session_key", sessionKey }, + })) + {} - QString algorithm() const { return content<QString>("algorithm"_ls); } - QString roomId() const { return content<QString>("room_id"_ls); } - QString sessionId() const { return content<QString>("session_id"_ls); } - QString sessionKey() const { return content<QString>("session_key"_ls); } + QUO_CONTENT_GETTER(QString, algorithm) + QUO_CONTENT_GETTER(QString, roomId) + QUO_CONTENT_GETTER(QString, sessionId) + QByteArray sessionKey() const + { + return contentPart<QString>("session_key"_ls).toLatin1(); + } }; -REGISTER_EVENT_TYPE(RoomKeyEvent) } // namespace Quotient diff --git a/lib/events/roommemberevent.cpp b/lib/events/roommemberevent.cpp index 3193a54d..4e7eae1b 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -1,47 +1,20 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Karol Kosek <krkkx@protonmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roommemberevent.h" - -#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") } -}; - 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 (const auto& ms = jv.toString(); !ms.isEmpty()) + return flagFromJsonString<Membership>(ms, MembershipStrings); - if (!membershipString.isEmpty()) - qCWarning(EVENTS) << "Unknown MembershipType: " << membershipString; - return MembershipType::Undefined; + qCWarning(EVENTS) << "Empty membership state"; + return Membership::Invalid; } }; } // namespace Quotient @@ -49,25 +22,29 @@ 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(sanitized(json["displayname"_ls].toString())) - , avatarUrl(json["avatar_url"_ls].toString()) + , displayName(fromJson<Omittable<QString>>(json["displayname"_ls])) + , avatarUrl(fromJson<Omittable<QString>>(json["avatar_url"_ls])) , reason(json["reason"_ls].toString()) -{} +{ + if (displayName) + displayName = sanitized(*displayName); +} -void MemberEventContent::fillJson(QJsonObject* o) const +QJsonObject MemberEventContent::toJson() 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]); - o->insert(QStringLiteral("displayname"), displayName); - if (avatarUrl.isValid()) - o->insert(QStringLiteral("avatar_url"), avatarUrl.toString()); + QJsonObject o; + if (membership != Membership::Invalid) + o.insert(QStringLiteral("membership"), + flagToJsonString(membership, MembershipStrings)); + if (displayName) + o.insert(QStringLiteral("displayname"), *displayName); + if (avatarUrl && avatarUrl->isValid()) + o.insert(QStringLiteral("avatar_url"), avatarUrl->toString()); if (!reason.isEmpty()) - o->insert(QStringLiteral("reason"), reason); + o.insert(QStringLiteral("reason"), reason); + return o; } bool RoomMemberEvent::changesMembership() const @@ -77,47 +54,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 : QString(); - return displayName() != prevName; + return prevContent() && prevContent()->displayName + ? newDisplayName() != *prevContent()->displayName + : newDisplayName().has_value(); } bool RoomMemberEvent::isAvatarUpdate() const { - auto prevAvatarUrl = prevContent() ? prevContent()->avatarUrl : QUrl(); - return avatarUrl() != prevAvatarUrl; + return prevContent() && prevContent()->avatarUrl + ? newAvatarUrl() != *prevContent()->avatarUrl + : newAvatarUrl().has_value(); } diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index 783b8207..9f063136 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -1,91 +1,57 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Karol Kosek <krkkx@protonmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "eventcontent.h" #include "stateevent.h" +#include "quotient_common.h" namespace Quotient { -class MemberEventContent : public EventContent::Base { +class QUOTIENT_API MemberEventContent { public: - enum MembershipType : size_t { - Invite = 0, - Join, - Knock, - Leave, - Ban, - Undefined - }; + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; - explicit MemberEventContent(MembershipType mt = Join) : membership(mt) {} + QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {} explicit MemberEventContent(const QJsonObject& json); + QJsonObject toJson() const; - MembershipType membership; + Membership membership; + /// (Only for invites) Whether the invite is to a direct chat bool isDirect = false; - QString displayName; - QUrl avatarUrl; + Omittable<QString> displayName; + Omittable<QUrl> avatarUrl; QString reason; - -protected: - void fillJson(QJsonObject* o) const override; }; -using MembershipType = MemberEventContent::MembershipType; +using MembershipType [[deprecated("Use Membership instead")]] = Membership; -class RoomMemberEvent : public StateEvent<MemberEventContent> { +class QUOTIENT_API RoomMemberEvent + : public KeyedStateEventBase<RoomMemberEvent, MemberEventContent> { Q_GADGET public: - DEFINE_EVENT_TYPEID("m.room.member", RoomMemberEvent) - - using MembershipType = MemberEventContent::MembershipType; + QUO_EVENT(RoomMemberEvent, "m.room.member") - explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) - {} - [[deprecated("Use RoomMemberEvent(userId, contentArgs) instead")]] - RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), QString(), c) - {} - template <typename... ArgTs> - RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) - : StateEvent(typeId(), matrixTypeId(), userId, - std::forward<ArgTs>(contentArgs)...) - {} + using MembershipType + [[deprecated("Use Quotient::Membership instead")]] = Membership; - /// A special constructor to create unknown RoomMemberEvents - /** - * This is needed in order to use RoomMemberEvent as a "base event - * class" in cases like GetMembersByRoomJob when RoomMemberEvents - * (rather than RoomEvents or StateEvents) are resolved from JSON. - * For such cases loadEvent<> requires an underlying class to be - * constructible with unknownTypeId() instead of its genuine id. - * Don't use it directly. - * \sa GetMembersByRoomJob, loadEvent, unknownTypeId - */ - RoomMemberEvent(Type type, const QJsonObject& fullJson) - : StateEvent(type, fullJson) - {} + using KeyedStateEventBase::KeyedStateEventBase; - MembershipType membership() const { return content().membership; } - QString userId() const { return fullJson()[StateKeyKeyL].toString(); } + Membership membership() const { return content().membership; } + QString userId() const { return stateKey(); } bool isDirect() const { return content().isDirect; } - QString displayName() const { return content().displayName; } - QUrl avatarUrl() const { return content().avatarUrl; } + Omittable<QString> newDisplayName() const { return content().displayName; } + Omittable<QUrl> newAvatarUrl() const { return content().avatarUrl; } + [[deprecated("Use newDisplayName() instead")]] QString displayName() const + { + return newDisplayName().value_or(QString()); + } + [[deprecated("Use newAvatarUrl() instead")]] QUrl avatarUrl() const + { + return newAvatarUrl().value_or(QUrl()); + } QString reason() const { return content().reason; } bool changesMembership() const; bool isBan() const; @@ -96,20 +62,5 @@ public: bool isLeave() const; bool isRename() const; bool isAvatarUpdate() const; - -private: - Q_ENUM(MembershipType) }; - -template <> -class EventFactory<RoomMemberEvent> { -public: - static event_ptr_tt<RoomMemberEvent> make(const QJsonObject& json, - const QString&) - { - return makeEvent<RoomMemberEvent>(json); - } -}; - -REGISTER_EVENT_TYPE(RoomMemberEvent) } // namespace Quotient diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index de499e7c..df4840b3 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -1,44 +1,33 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roommessageevent.h" #include "logging.h" +#include "events/eventrelation.h" #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; using MsgType = RoomMessageEvent::MsgType; -static const auto RelatesToKeyL = "m.relates_to"_ls; -static const auto MsgTypeKeyL = "msgtype"_ls; -static const auto FormattedBodyKeyL = "formatted_body"_ls; - -static const auto TextTypeKey = "m.text"; -static const auto EmoteTypeKey = "m.emote"; -static const auto NoticeTypeKey = "m.notice"; - -static const auto HtmlContentTypeId = QStringLiteral("org.matrix.custom.html"); +namespace { // Supporting internal definitions +constexpr auto RelatesToKey = "m.relates_to"_ls; +constexpr auto MsgTypeKey = "msgtype"_ls; +constexpr auto FormattedBodyKey = "formatted_body"_ls; +constexpr auto TextTypeKey = "m.text"_ls; +constexpr auto EmoteTypeKey = "m.emote"_ls; +constexpr auto NoticeTypeKey = "m.notice"_ls; +constexpr auto HtmlContentTypeId = "org.matrix.custom.html"_ls; template <typename ContentT> TypedBase* make(const QJsonObject& json) @@ -49,13 +38,13 @@ TypedBase* make(const QJsonObject& json) template <> TypedBase* make<TextContent>(const QJsonObject& json) { - return json.contains(FormattedBodyKeyL) || json.contains(RelatesToKeyL) + return json.contains(FormattedBodyKey) || json.contains(RelatesToKey) ? new TextContent(json) : nullptr; } struct MsgTypeDesc { - QString matrixType; + QLatin1String matrixType; MsgType enumType; TypedBase* (*maker)(const QJsonObject&); }; @@ -64,11 +53,11 @@ const std::vector<MsgTypeDesc> msgTypes = { { TextTypeKey, MsgType::Text, make<TextContent> }, { EmoteTypeKey, MsgType::Emote, make<TextContent> }, { NoticeTypeKey, MsgType::Notice, make<TextContent> }, - { QStringLiteral("m.image"), MsgType::Image, make<ImageContent> }, - { QStringLiteral("m.file"), MsgType::File, make<FileContent> }, - { QStringLiteral("m.location"), MsgType::Location, make<LocationContent> }, - { QStringLiteral("m.video"), MsgType::Video, make<VideoContent> }, - { QStringLiteral("m.audio"), MsgType::Audio, make<AudioContent> } + { "m.image"_ls, MsgType::Image, make<ImageContent> }, + { "m.file"_ls, MsgType::File, make<FileContent> }, + { "m.location"_ls, MsgType::Location, make<LocationContent> }, + { "m.video"_ls, MsgType::Video, make<VideoContent> }, + { "m.audio"_ls, MsgType::Audio, make<AudioContent> } }; QString msgTypeToJson(MsgType enumType) @@ -95,49 +84,52 @@ MsgType jsonToMsgType(const QString& matrixType) return MsgType::Unknown; } -inline bool isReplacement(const Omittable<RelatesTo>& rel) +inline bool isReplacement(const Omittable<EventRelation>& rel) { - return rel && rel->type == RelatesTo::ReplacementTypeId(); + return rel && rel->type == EventRelation::ReplacementType; } +} // anonymous namespace + QJsonObject RoomMessageEvent::assembleContentJson(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) { - auto json = content ? content->toJson() : QJsonObject(); - if (json.contains(RelatesToKeyL)) { + QJsonObject json; + if (content) { + // TODO: replace with content->fillJson(json) when it starts working + json = content->toJson(); if (jsonMsgType != TextTypeKey && jsonMsgType != NoticeTypeKey && jsonMsgType != EmoteTypeKey) { - json.remove(RelatesToKeyL); - qCWarning(EVENTS) - << RelatesToKeyL << "cannot be used in" << jsonMsgType - << "messages; the relation has been stripped off"; - } else { - // After the above, we know for sure that the content is TextContent - // and that its RelatesTo structure is not omitted - auto* textContent = static_cast<const TextContent*>(content); - Q_ASSERT(textContent && textContent->relatesTo.has_value()); - if (textContent->relatesTo->type == RelatesTo::ReplacementTypeId()) { - auto newContentJson = json.take("m.new_content"_ls).toObject(); - newContentJson.insert(BodyKey, plainBody); - newContentJson.insert(MsgTypeKeyL, jsonMsgType); - json.insert(QStringLiteral("m.new_content"), newContentJson); - json[MsgTypeKeyL] = jsonMsgType; - json[BodyKeyL] = "* " + plainBody; - return json; + if (json.contains(RelatesToKey)) { + json.remove(RelatesToKey); + qCWarning(EVENTS) + << RelatesToKey << "cannot be used in" << jsonMsgType + << "messages; the relation has been stripped off"; } + } else if (auto* textContent = static_cast<const TextContent*>(content); + textContent->relatesTo + && textContent->relatesTo->type + == EventRelation::ReplacementType) { + auto newContentJson = json.take("m.new_content"_ls).toObject(); + newContentJson.insert(BodyKey, plainBody); + newContentJson.insert(MsgTypeKey, jsonMsgType); + json.insert(QStringLiteral("m.new_content"), newContentJson); + json[MsgTypeKey] = jsonMsgType; + json[BodyKeyL] = "* " + plainBody; + return json; } } - json.insert(QStringLiteral("msgtype"), jsonMsgType); - json.insert(QStringLiteral("body"), plainBody); + json.insert(MsgTypeKey, jsonMsgType); + json.insert(BodyKey, plainBody); return json; } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, const QString& jsonMsgType, TypedBase* content) - : RoomEvent(typeId(), matrixTypeId(), - assembleContentJson(plainBody, jsonMsgType, content)) + : RoomEvent( + basicJson(TypeId, assembleContentJson(plainBody, jsonMsgType, content))) , _content(content) {} @@ -146,6 +138,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(); @@ -179,15 +172,16 @@ RoomMessageEvent::RoomMessageEvent(const QString& plainBody, : rawMsgTypeForFile(file), contentFromFile(file, asGenericFile)) {} +#endif RoomMessageEvent::RoomMessageEvent(const QJsonObject& obj) - : RoomEvent(typeId(), obj), _content(nullptr) + : RoomEvent(obj), _content(nullptr) { if (isRedacted()) return; const QJsonObject content = contentJson(); - if (content.contains(MsgTypeKeyL) && content.contains(BodyKeyL)) { - auto msgtype = content[MsgTypeKeyL].toString(); + if (content.contains(MsgTypeKey) && content.contains(BodyKeyL)) { + auto msgtype = content[MsgTypeKey].toString(); bool msgTypeFound = false; for (const auto& mt : msgTypes) if (mt.matrixType == msgtype) { @@ -213,12 +207,12 @@ RoomMessageEvent::MsgType RoomMessageEvent::msgtype() const QString RoomMessageEvent::rawMsgtype() const { - return contentJson()[MsgTypeKeyL].toString(); + return contentPart<QString>(MsgTypeKey); } QString RoomMessageEvent::plainBody() const { - return contentJson()[BodyKeyL].toString(); + return contentPart<QString>(BodyKeyL); } QMimeType RoomMessageEvent::mimeType() const @@ -276,7 +270,7 @@ QString RoomMessageEvent::rawMsgTypeForFile(const QFileInfo& fi) } TextContent::TextContent(QString text, const QString& contentType, - Omittable<RelatesTo> relatesTo) + Omittable<EventRelation> relatesTo) : mimeType(QMimeDatabase().mimeTypeForName(contentType)) , body(std::move(text)) , relatesTo(std::move(relatesTo)) @@ -285,26 +279,8 @@ TextContent::TextContent(QString text, const QString& contentType, mimeType = QMimeDatabase().mimeTypeForName("text/html"); } -namespace Quotient { -// Overload the default fromJson<> logic that defined in converters.h -// as we want -template <> -Omittable<RelatesTo> fromJson(const QJsonValue& jv) -{ - const auto jo = jv.toObject(); - if (jo.isEmpty()) - return none; - const auto replyJson = jo.value(RelatesTo::ReplyTypeId()).toObject(); - if (!replyJson.isEmpty()) - return replyTo(fromJson<QString>(replyJson[EventIdKeyL])); - - return RelatesTo { jo.value("rel_type"_ls).toString(), - jo.value(EventIdKeyL).toString() }; -} -} // namespace Quotient - TextContent::TextContent(const QJsonObject& json) - : relatesTo(fromJson<Omittable<RelatesTo>>(json[RelatesToKeyL])) + : relatesTo(fromJson<Omittable<EventRelation>>(json[RelatesToKey])) { QMimeDatabase db; static const auto PlainTextMimeType = db.mimeTypeForName("text/plain"); @@ -317,7 +293,7 @@ TextContent::TextContent(const QJsonObject& json) // of sending HTML messages. if (actualJson["format"_ls].toString() == HtmlContentTypeId) { mimeType = HtmlMimeType; - body = actualJson[FormattedBodyKeyL].toString(); + body = actualJson[FormattedBodyKey].toString(); } else { // Falling back to plain text, as there's no standard way to describe // rich text in messages. @@ -326,29 +302,30 @@ TextContent::TextContent(const QJsonObject& json) } } -void TextContent::fillJson(QJsonObject* json) const +void TextContent::fillJson(QJsonObject &json) const { static const auto FormatKey = QStringLiteral("format"); - static const auto FormattedBodyKey = QStringLiteral("formatted_body"); - Q_ASSERT(json); if (mimeType.inherits("text/html")) { - json->insert(FormatKey, HtmlContentTypeId); - json->insert(FormattedBodyKey, body); + json.insert(FormatKey, HtmlContentTypeId); + json.insert(FormattedBodyKey, body); } if (relatesTo) { - json->insert(QStringLiteral("m.relates_to"), - relatesTo->type == RelatesTo::ReplyTypeId() ? - QJsonObject { { relatesTo->type, QJsonObject{ { EventIdKey, relatesTo->eventId } } } } : - QJsonObject { { "rel_type", relatesTo->type }, { EventIdKey, relatesTo->eventId } } - ); - if (relatesTo->type == RelatesTo::ReplacementTypeId()) { + json.insert( + QStringLiteral("m.relates_to"), + relatesTo->type == EventRelation::ReplyType + ? QJsonObject { { relatesTo->type, + QJsonObject { + { EventIdKey, relatesTo->eventId } } } } + : QJsonObject { { RelTypeKey, relatesTo->type }, + { EventIdKey, relatesTo->eventId } }); + if (relatesTo->type == EventRelation::ReplacementType) { QJsonObject newContentJson; if (mimeType.inherits("text/html")) { newContentJson.insert(FormatKey, HtmlContentTypeId); newContentJson.insert(FormattedBodyKey, body); } - json->insert(QStringLiteral("m.new_content"), newContentJson); + json.insert(QStringLiteral("m.new_content"), newContentJson); } } } @@ -369,9 +346,8 @@ QMimeType LocationContent::type() const return QMimeDatabase().mimeTypeForData(geoUri.toLatin1()); } -void LocationContent::fillJson(QJsonObject* o) const +void LocationContent::fillJson(QJsonObject& o) const { - Q_ASSERT(o); - o->insert(QStringLiteral("geo_uri"), geoUri); - o->insert(QStringLiteral("info"), toInfoJson(thumbnail)); + o.insert(QStringLiteral("geo_uri"), geoUri); + o.insert(QStringLiteral("info"), toInfoJson(thumbnail)); } diff --git a/lib/events/roommessageevent.h b/lib/events/roommessageevent.h index 2501d097..889fc4dc 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -1,24 +1,12 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "eventcontent.h" +#include "eventrelation.h" #include "roomevent.h" class QFileInfo; @@ -29,14 +17,10 @@ namespace MessageEventContent = EventContent; // Back-compatibility /** * The event class corresponding to m.room.message events */ -class RoomMessageEvent : public RoomEvent { +class QUOTIENT_API 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) + QUO_EVENT(RoomMessageEvent, "m.room.message") enum class MsgType { Text, @@ -55,8 +39,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; @@ -71,9 +59,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); @@ -89,47 +94,49 @@ private: Q_ENUM(MsgType) }; -REGISTER_EVENT_TYPE(RoomMessageEvent) + using MessageEventType = RoomMessageEvent::MsgType; namespace EventContent { - // Additional event content types - struct RelatesTo { - static constexpr const char* ReplyTypeId() { return "m.in_reply_to"; } - static constexpr const char* ReplacementTypeId() { return "m.replace"; } - QString type; // The only supported relation so far - QString eventId; + struct [[deprecated("Use Quotient::EventRelation instead")]] RelatesTo + : EventRelation { + static constexpr auto ReplyTypeId() { return ReplyType; } + static constexpr auto ReplacementTypeId() { return ReplacementType; } }; - inline RelatesTo replyTo(QString eventId) + [[deprecated("Use EventRelation::replyTo() instead")]] + inline auto replyTo(QString eventId) { - return { RelatesTo::ReplyTypeId(), std::move(eventId) }; + return EventRelation::replyTo(std::move(eventId)); } - inline RelatesTo replacementOf(QString eventId) + [[deprecated("Use EventRelation::replace() instead")]] + inline auto replacementOf(QString eventId) { - return { RelatesTo::ReplacementTypeId(), std::move(eventId) }; + return EventRelation::replace(std::move(eventId)); } + // Additional event content types + /** * Rich text content for m.text, m.emote, m.notice * * Available fields: mimeType, body. The body can be either rich text * or plain text, depending on what mimeType specifies. */ - class TextContent : public TypedBase { + class QUOTIENT_API TextContent : public TypedBase { public: TextContent(QString text, const QString& contentType, - Omittable<RelatesTo> relatesTo = none); + Omittable<EventRelation> relatesTo = none); explicit TextContent(const QJsonObject& json); QMimeType type() const override { return mimeType; } QMimeType mimeType; QString body; - Omittable<RelatesTo> relatesTo; + Omittable<EventRelation> relatesTo; protected: - void fillJson(QJsonObject* json) const override; + void fillJson(QJsonObject& json) const override; }; /** @@ -145,7 +152,7 @@ namespace EventContent { * - thumbnail.mimeType * - thumbnail.imageSize */ - class LocationContent : public TypedBase { + class QUOTIENT_API LocationContent : public TypedBase { public: LocationContent(const QString& geoUri, const Thumbnail& thumbnail = {}); explicit LocationContent(const QJsonObject& json); @@ -157,28 +164,25 @@ namespace EventContent { Thumbnail thumbnail; protected: - void fillJson(QJsonObject* o) const override; + void fillJson(QJsonObject& o) const override; }; /** * A base class for info types that include duration: audio and video */ - template <typename ContentT> - class PlayableContent : public ContentT { + template <typename InfoT> + class PlayableContent : public UrlBasedContent<InfoT> { public: - using ContentT::ContentT; + using UrlBasedContent<InfoT>::UrlBasedContent; PlayableContent(const QJsonObject& json) - : ContentT(json) - , duration(ContentT::originalInfoJson["duration"_ls].toInt()) + : UrlBasedContent<InfoT>(json) + , duration(FileInfo::originalInfoJson["duration"_ls].toInt()) {} protected: - void fillJson(QJsonObject* json) const override + void fillInfoJson(QJsonObject& infoJson) const override { - ContentT::fillJson(json); - auto infoJson = json->take("info"_ls).toObject(); infoJson.insert(QStringLiteral("duration"), duration); - json->insert(QStringLiteral("info"), infoJson); } public: @@ -204,7 +208,7 @@ namespace EventContent { * - mimeType * - imageSize */ - using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>; + using VideoContent = PlayableContent<ImageInfo>; /** * Content class for m.audio @@ -217,7 +221,13 @@ namespace EventContent { * - payloadSize ("size" in JSON) * - mimeType ("mimetype" in JSON) * - duration + * - thumbnail.url ("thumbnail_url" in JSON - extension to the spec) + * - corresponding to the "info/thumbnail_info" subobject: contents of + * thumbnail field (extension to the spec): + * - payloadSize + * - mimeType + * - imageSize */ - using AudioContent = PlayableContent<UrlBasedContent<FileInfo>>; + using AudioContent = PlayableContent<FileInfo>; } // namespace EventContent } // namespace Quotient diff --git a/lib/events/roompowerlevelsevent.cpp b/lib/events/roompowerlevelsevent.cpp index 0a401752..d9bd010b 100644 --- a/lib/events/roompowerlevelsevent.cpp +++ b/lib/events/roompowerlevelsevent.cpp @@ -1,9 +1,12 @@ -#include "roompowerlevelsevent.h" +// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org> +// SPDX-License-Identifier: LGPL-2.1-or-later -#include <QJsonDocument> +#include "roompowerlevelsevent.h" using namespace Quotient; +// The default values used below are defined in +// https://spec.matrix.org/v1.3/client-server-api/#mroompower_levels PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : invite(json["invite"_ls].toInt(50)), kick(json["kick"_ls].toInt(50)), @@ -15,48 +18,36 @@ PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : users(fromJson<QHash<QString, int>>(json["users"_ls])), usersDefault(json["users_default"_ls].toInt(0)), notifications(Notifications{json["notifications"_ls].toObject()["room"_ls].toInt(50)}) -{ -} +{} -void PowerLevelsEventContent::fillJson(QJsonObject* o) const { - o->insert(QStringLiteral("invite"), invite); - o->insert(QStringLiteral("kick"), kick); - o->insert(QStringLiteral("ban"), ban); - o->insert(QStringLiteral("redact"), redact); - o->insert(QStringLiteral("events"), Quotient::toJson(events)); - o->insert(QStringLiteral("events_default"), eventsDefault); - o->insert(QStringLiteral("state_default"), stateDefault); - o->insert(QStringLiteral("users"), Quotient::toJson(users)); - o->insert(QStringLiteral("users_default"), usersDefault); - o->insert(QStringLiteral("notifications"), QJsonObject{{"room", notifications.room}}); +QJsonObject PowerLevelsEventContent::toJson() const +{ + QJsonObject o; + o.insert(QStringLiteral("invite"), invite); + o.insert(QStringLiteral("kick"), kick); + o.insert(QStringLiteral("ban"), ban); + o.insert(QStringLiteral("redact"), redact); + o.insert(QStringLiteral("events"), Quotient::toJson(events)); + o.insert(QStringLiteral("events_default"), eventsDefault); + o.insert(QStringLiteral("state_default"), stateDefault); + o.insert(QStringLiteral("users"), Quotient::toJson(users)); + o.insert(QStringLiteral("users_default"), usersDefault); + o.insert(QStringLiteral("notifications"), + QJsonObject { { "room", notifications.room } }); + return o; } -int RoomPowerLevelsEvent::powerLevelForEvent(const QString &eventId) const { - auto e = events(); - - if (e.contains(eventId)) { - return e[eventId]; - } - - return eventsDefault(); +int RoomPowerLevelsEvent::powerLevelForEvent(const QString& eventId) const +{ + return events().value(eventId, eventsDefault()); } -int RoomPowerLevelsEvent::powerLevelForState(const QString &eventId) const { - auto e = events(); - - if (e.contains(eventId)) { - return e[eventId]; - } - - return stateDefault(); +int RoomPowerLevelsEvent::powerLevelForState(const QString& eventId) const +{ + return events().value(eventId, stateDefault()); } -int RoomPowerLevelsEvent::powerLevelForUser(const QString &userId) const { - auto u = users(); - - if (u.contains(userId)) { - return u[userId]; - } - - return usersDefault(); +int RoomPowerLevelsEvent::powerLevelForUser(const QString& userId) const +{ + return users().value(userId, usersDefault()); } diff --git a/lib/events/roompowerlevelsevent.h b/lib/events/roompowerlevelsevent.h index f0f7207f..6150980a 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -1,16 +1,18 @@ +// SPDX-FileCopyrightText: 2019 Black Hat <bhat@encom.eu.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once -#include "eventcontent.h" #include "stateevent.h" namespace Quotient { -class PowerLevelsEventContent : public EventContent::Base { -public: +struct QUOTIENT_API PowerLevelsEventContent { struct Notifications { int room; }; explicit PowerLevelsEventContent(const QJsonObject& json); + QJsonObject toJson() const; int invite; int kick; @@ -26,19 +28,14 @@ public: int usersDefault; Notifications notifications; - -protected: - void fillJson(QJsonObject* o) const override; }; -class RoomPowerLevelsEvent : public StateEvent<PowerLevelsEventContent> { - Q_GADGET +class QUOTIENT_API RoomPowerLevelsEvent + : public KeylessStateEventBase<RoomPowerLevelsEvent, PowerLevelsEventContent> { public: - DEFINE_EVENT_TYPEID("m.room.power_levels", RoomPowerLevelsEvent) + QUO_EVENT(RoomPowerLevelsEvent, "m.room.power_levels") - explicit RoomPowerLevelsEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj) - {} + using KeylessStateEventBase::KeylessStateEventBase; int invite() const { return content().invite; } int kick() const { return content().kick; } @@ -58,19 +55,5 @@ public: int powerLevelForEvent(const QString& eventId) const; int powerLevelForState(const QString& eventId) const; int powerLevelForUser(const QString& userId) const; - -private: }; - -template <> -class EventFactory<RoomPowerLevelsEvent> { -public: - static event_ptr_tt<RoomPowerLevelsEvent> make(const QJsonObject& json, - const QString&) - { - return makeEvent<RoomPowerLevelsEvent>(json); - } -}; - -REGISTER_EVENT_TYPE(RoomPowerLevelsEvent) } // namespace Quotient diff --git a/lib/events/roomtombstoneevent.cpp b/lib/events/roomtombstoneevent.cpp index f93eb60d..2c3492d6 100644 --- a/lib/events/roomtombstoneevent.cpp +++ b/lib/events/roomtombstoneevent.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2019 QMatrixClient project - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "roomtombstoneevent.h" @@ -22,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/roomtombstoneevent.h b/lib/events/roomtombstoneevent.h index 2c2f0663..c85b4dfd 100644 --- a/lib/events/roomtombstoneevent.h +++ b/lib/events/roomtombstoneevent.h @@ -1,37 +1,18 @@ -/****************************************************************************** - * Copyright (C) 2019 QMatrixClient project - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "stateevent.h" namespace Quotient { -class RoomTombstoneEvent : public StateEventBase { +class QUOTIENT_API RoomTombstoneEvent : public StateEvent { public: - DEFINE_EVENT_TYPEID("m.room.tombstone", RoomTombstoneEvent) + QUO_EVENT(RoomTombstoneEvent, "m.room.tombstone") - explicit RoomTombstoneEvent() : StateEventBase(typeId(), matrixTypeId()) {} - explicit RoomTombstoneEvent(const QJsonObject& obj) - : StateEventBase(typeId(), obj) - {} + using StateEvent::StateEvent; QString serverMessage() const; QString successorRoomId() const; }; -REGISTER_EVENT_TYPE(RoomTombstoneEvent) } // namespace Quotient diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index cde5b0fd..2a0d3817 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -1,89 +1,47 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "stateevent.h" +#include "single_key_value.h" namespace Quotient { -namespace EventContent { - template <typename T> - class SimpleContent { - public: - using value_type = T; - - // The constructor is templated to enable perfect forwarding - template <typename TT> - SimpleContent(QString keyName, TT&& value) - : value(std::forward<TT>(value)), key(std::move(keyName)) - {} - SimpleContent(const QJsonObject& json, QString keyName) - : value(fromJson<T>(json[keyName])), key(std::move(keyName)) - {} - QJsonObject toJson() const - { - return { { key, Quotient::toJson(value) } }; - } - - public: - T value; - - protected: - QString key; - }; -} // namespace EventContent - -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ - class _Name : public StateEvent<EventContent::SimpleContent<_ValueType>> { \ - public: \ - using value_type = content_type::value_type; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name() : _Name(value_type()) {} \ - template <typename T> \ - explicit _Name(T&& value) \ - : StateEvent(typeId(), matrixTypeId(), QString(), \ - QStringLiteral(#_ContentKey), std::forward<T>(value)) \ - {} \ - explicit _Name(QJsonObject obj) \ - : StateEvent(typeId(), std::move(obj), \ - QStringLiteral(#_ContentKey)) \ - {} \ - auto _ContentKey() const { return content().value; } \ - }; \ - REGISTER_EVENT_TYPE(_Name) \ - // End of macro +#define DEFINE_SIMPLE_STATE_EVENT(Name_, TypeId_, ValueType_, ContentKey_) \ + constexpr auto Name_##Key = #ContentKey_##_ls; \ + class QUOTIENT_API Name_ \ + : public KeylessStateEventBase< \ + Name_, EventContent::SingleKeyValue<ValueType_, Name_##Key>> { \ + public: \ + using value_type = ValueType_; \ + QUO_EVENT(Name_, TypeId_) \ + using KeylessStateEventBase::KeylessStateEventBase; \ + auto ContentKey_() const { return content().value; } \ + }; \ +// End of macro DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) - -class RoomAliasesEvent - : public StateEvent<EventContent::SimpleContent<QStringList>> { +DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", + QStringList, pinnedEvents) + +constexpr auto RoomAliasesEventKey = "aliases"_ls; +class QUOTIENT_API RoomAliasesEvent + : public KeyedStateEventBase< + RoomAliasesEvent, + EventContent::SingleKeyValue<QStringList, RoomAliasesEventKey>> +{ 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) - {} + QUO_EVENT(RoomAliasesEvent, "m.room.aliases") + using KeyedStateEventBase::KeyedStateEventBase; + + Q_DECL_DEPRECATED_X( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") QString server() const { return stateKey(); } + Q_DECL_DEPRECATED_X( + "m.room.aliases events are deprecated by the Matrix spec; use" + " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases") QStringList aliases() const { return content().value; } }; -REGISTER_EVENT_TYPE(RoomAliasesEvent) } // namespace Quotient diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h new file mode 100644 index 00000000..ca2bd331 --- /dev/null +++ b/lib/events/single_key_value.h @@ -0,0 +1,36 @@ +#pragma once + +#include "converters.h" + +namespace Quotient { + +namespace EventContent { + template <typename T, const QLatin1String& KeyStr> + struct SingleKeyValue { + // NOLINTBEGIN(google-explicit-constructor): that check should learn + // about explicit(false) + QUO_IMPLICIT SingleKeyValue(const T& v = {}) + : value { v } + {} + QUO_IMPLICIT SingleKeyValue(T&& v) + : value { std::move(v) } + {} + // NOLINTEND(google-explicit-constructor) + T value; + }; +} // namespace EventContent + +template <typename ValueT, const QLatin1String& KeyStr> +struct JsonConverter<EventContent::SingleKeyValue<ValueT, KeyStr>> { + using content_type = EventContent::SingleKeyValue<ValueT, KeyStr>; + static content_type load(const QJsonValue& jv) + { + return { fromJson<ValueT>(jv.toObject().value(JsonKey)) }; + } + static QJsonObject dump(const content_type& c) + { + return { { JsonKey, toJson(c.value) } }; + } + static inline const auto JsonKey = toSnakeCase(KeyStr); +}; +} // namespace Quotient diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 5909e8a6..72ecd5ad 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -1,64 +1,40 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "stateevent.h" +#include "logging.h" 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); - }); +StateEvent::StateEvent(const QJsonObject& json) + : RoomEvent(json) +{ + Q_ASSERT_X(json.contains(StateKeyKeyL), __FUNCTION__, + "Attempt to create a state event without state key"); +} -StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, - const QString& stateKey, +StateEvent::StateEvent(Event::Type type, const QString& stateKey, const QJsonObject& contentJson) - : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey)) + : RoomEvent(basicJson(type, stateKey, contentJson)) {} -bool StateEventBase::repeatsState() const +bool StateEvent::repeatsState() const { - const auto prevContentJson = unsignedJson().value(PrevContentKeyL); - return fullJson().value(ContentKeyL) == prevContentJson; + return contentJson() == unsignedPart<QJsonObject>(PrevContentKeyL); } -QString StateEventBase::replacedState() const +QString StateEvent::replacedState() const { - return unsignedJson().value("replaces_state"_ls).toString(); + return unsignedPart<QString>("replaces_state"_ls); } -void StateEventBase::dumpTo(QDebug dbg) const +void StateEvent::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 710b4271..992ec2e2 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -22,41 +7,54 @@ namespace Quotient { -/// Make a minimal correct Matrix state event JSON -template <typename StrT> -inline QJsonObject basicStateEventJson(StrT matrixType, - const QJsonObject& content, - const QString& stateKey = {}) -{ - return { { TypeKey, std::forward<StrT>(matrixType) }, - { StateKeyKey, stateKey }, - { ContentKey, content } }; -} - -class StateEventBase : public RoomEvent { +class QUOTIENT_API StateEvent : public RoomEvent { public: - using factory_t = EventFactory<StateEventBase>; + QUO_BASE_EVENT(StateEvent, "json.contains('state_key')"_ls, + RoomEvent::BaseMetaType) + static bool isValid(const QJsonObject& fullJson) + { + return fullJson.contains(StateKeyKeyL); + } - StateEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) - {} - StateEventBase(Type type, event_mtype_t matrixType, - const QString& stateKey = {}, - const QJsonObject& contentJson = {}); - ~StateEventBase() override = default; + //! \brief Static setting of whether a given even type uses state keys + //! + //! Most event types don't use a state key; overriding this to `true` + //! for a given type changes the calls across Quotient to include state key + //! in their signatures; otherwise, state key is still accessible but + //! constructors and calls in, e.g., RoomStateView don't include it. + static constexpr auto needsStateKey = false; + + explicit StateEvent(Type type, const QString& stateKey = {}, + const QJsonObject& contentJson = {}); + + //! Make a minimal correct Matrix state event JSON + static QJsonObject basicJson(const QString& matrixTypeId, + const QString& stateKey = {}, + const QJsonObject& contentJson = {}) + { + return { { TypeKey, matrixTypeId }, + { StateKeyKey, stateKey }, + { ContentKey, contentJson } }; + } - bool isStateEvent() const override { return true; } QString replacedState() const; - void dumpTo(QDebug dbg) const override; - virtual bool repeatsState() const; + +protected: + explicit StateEvent(const QJsonObject& json); + void dumpTo(QDebug dbg) const override; }; -using StateEventPtr = event_ptr_tt<StateEventBase>; -using StateEvents = EventsArray<StateEventBase>; +using StateEventBase + [[deprecated("StateEventBase is StateEvent now")]] = StateEvent; +using StateEventPtr = event_ptr_tt<StateEvent>; +using StateEvents = EventsArray<StateEvent>; -template <> -inline bool is<StateEventBase>(const Event& e) +[[deprecated("Use StateEvent::basicJson() instead")]] +inline QJsonObject basicStateEventJson(const QString& matrixTypeId, + const QJsonObject& content, + const QString& stateKey = {}) { - return e.isStateEvent(); + return StateEvent::basicJson(matrixTypeId, stateKey, content); } /** @@ -65,67 +63,89 @@ inline bool is<StateEventBase>(const Event& e) * \sa * https://matrix.org/docs/spec/client_server/unstable.html#types-of-room-events */ -using StateEventKey = QPair<QString, QString>; - -template <typename ContentT> -struct Prev { - template <typename... ContentParamTs> - explicit Prev(const QJsonObject& unsignedJson, - ContentParamTs&&... contentParams) - : senderId(unsignedJson.value("prev_sender"_ls).toString()) - , content(unsignedJson.value(PrevContentKeyL).toObject(), - std::forward<ContentParamTs>(contentParams)...) - {} - - QString senderId; - ContentT content; -}; +using StateEventKey = std::pair<QString, QString>; -template <typename ContentT> -class StateEvent : public StateEventBase { +template <typename EventT, typename ContentT> +class EventTemplate<EventT, StateEvent, ContentT> + : public StateEvent { public: using content_type = ContentT; + struct Prev { + explicit Prev() = default; + explicit Prev(const QJsonObject& unsignedJson) + : senderId(fromJson<QString>(unsignedJson["prev_sender"_ls])) + , content( + fromJson<Omittable<ContentT>>(unsignedJson[PrevContentKeyL])) + {} + + QString senderId; + Omittable<ContentT> content; + }; + + explicit EventTemplate(const QJsonObject& fullJson) + : StateEvent(fullJson) + , _content(fromJson<ContentT>(Event::contentJson())) + , _prev(unsignedJson()) + {} template <typename... ContentParamTs> - explicit StateEvent(Type type, const QJsonObject& fullJson, - ContentParamTs&&... contentParams) - : StateEventBase(type, fullJson) - , _content(contentJson(), std::forward<ContentParamTs>(contentParams)...) - { - const auto& unsignedData = unsignedJson(); - if (unsignedData.contains(PrevContentKeyL)) - _prev = std::make_unique<Prev<ContentT>>( - unsignedData, std::forward<ContentParamTs>(contentParams)...); - } - template <typename... ContentParamTs> - explicit StateEvent(Type type, event_mtype_t matrixType, - const QString& stateKey, - ContentParamTs&&... contentParams) - : StateEventBase(type, matrixType, stateKey) - , _content(std::forward<ContentParamTs>(contentParams)...) + explicit EventTemplate(const QString& stateKey, + ContentParamTs&&... contentParams) + : StateEvent(EventT::TypeId, stateKey) + , _content { std::forward<ContentParamTs>(contentParams)... } { - editJson().insert(ContentKey, _content.toJson()); + editJson().insert(ContentKey, toJson(_content)); } const ContentT& content() const { return _content; } + template <typename VisitorT> void editContent(VisitorT&& visitor) { visitor(_content); - editJson()[ContentKeyL] = _content.toJson(); - } - [[deprecated("Use prevContent instead")]] const ContentT* prev_content() const - { - return prevContent(); - } - const ContentT* prevContent() const - { - return _prev ? &_prev->content : nullptr; + editJson()[ContentKeyL] = toJson(_content); } - QString prevSenderId() const { return _prev ? _prev->senderId : QString(); } + const Omittable<ContentT>& prevContent() const { return _prev.content; } + QString prevSenderId() const { return _prev.senderId; } private: ContentT _content; - std::unique_ptr<Prev<ContentT>> _prev; + Prev _prev; +}; + +template <typename EventT, typename ContentT> +class KeyedStateEventBase + : public EventTemplate<EventT, StateEvent, ContentT> { +public: + static constexpr auto needsStateKey = true; + + using EventTemplate<EventT, StateEvent, ContentT>::EventTemplate; }; + +template <typename EvT> +concept Keyed_State_Event = EvT::needsStateKey; + +template <typename EventT, typename ContentT> +class KeylessStateEventBase + : public EventTemplate<EventT, StateEvent, ContentT> { +private: + using base_type = EventTemplate<EventT, StateEvent, ContentT>; + +public: + template <typename... ContentParamTs> + explicit KeylessStateEventBase(ContentParamTs&&... contentParams) + : base_type(QString(), std::forward<ContentParamTs>(contentParams)...) + {} + +protected: + explicit KeylessStateEventBase(const QJsonObject& fullJson) + : base_type(fullJson) + {} +}; + +template <typename EvT> +concept Keyless_State_Event = !EvT::needsStateKey; + } // namespace Quotient +Q_DECLARE_METATYPE(Quotient::StateEvent*) +Q_DECLARE_METATYPE(const Quotient::StateEvent*) diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h new file mode 100644 index 00000000..67905481 --- /dev/null +++ b/lib/events/stickerevent.h @@ -0,0 +1,48 @@ +// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "roomevent.h" +#include "eventcontent.h" + +namespace Quotient { + +/// Sticker messages are specialised image messages that are displayed without +/// controls (e.g. no "download" link, or light-box view on click, as would be +/// displayed for for m.image events). +class QUOTIENT_API StickerEvent : public RoomEvent +{ +public: + QUO_EVENT(StickerEvent, "m.sticker") + + explicit StickerEvent(const QJsonObject& obj) + : RoomEvent(TypeId, obj) + , m_imageContent( + EventContent::ImageContent(obj["content"_ls].toObject())) + {} + + /// \brief A textual representation or associated description of the + /// sticker image. + /// + /// This could be the alt text of the original image, or a message to + /// accompany and further describe the sticker. + QUO_CONTENT_GETTER(QString, body) + + /// \brief Metadata about the image referred to in url including a + /// thumbnail representation. + const EventContent::ImageContent& image() const + { + return m_imageContent; + } + + /// \brief The URL to the sticker image. This must be a valid mxc:// URI. + QUrl url() const + { + return m_imageContent.url(); + } + +private: + EventContent::ImageContent m_imageContent; +}; +} // namespace Quotient diff --git a/lib/events/typingevent.cpp b/lib/events/typingevent.cpp deleted file mode 100644 index a95d2f0d..00000000 --- a/lib/events/typingevent.cpp +++ /dev/null @@ -1,31 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#include "typingevent.h" - -#include <QtCore/QJsonArray> - -using namespace Quotient; - -TypingEvent::TypingEvent(const QJsonObject& obj) : Event(typeId(), obj) -{ - const auto& array = contentJson()["user_ids"_ls].toArray(); - _users.reserve(array.size()); - for (const auto& user : array) - _users.push_back(user.toString()); -} diff --git a/lib/events/typingevent.h b/lib/events/typingevent.h index 1cf4e69d..b56475af 100644 --- a/lib/events/typingevent.h +++ b/lib/events/typingevent.h @@ -1,36 +1,10 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "event.h" namespace Quotient { -class TypingEvent : public Event { -public: - DEFINE_EVENT_TYPEID("m.typing", TypingEvent) - - TypingEvent(const QJsonObject& obj); - - const QStringList& users() const { return _users; } - -private: - QStringList _users; -}; -REGISTER_EVENT_TYPE(TypingEvent) +DEFINE_SIMPLE_EVENT(TypingEvent, Event, "m.typing", QStringList, users, "user_ids") } // namespace Quotient 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..a10c81fb --- /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 QUOTIENT_API 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; +}; + +QUOTIENT_API QDebug operator<<(QDebug dbg, const EventStats& es); + +} diff --git a/lib/expected.h b/lib/expected.h new file mode 100644 index 00000000..81e186ea --- /dev/null +++ b/lib/expected.h @@ -0,0 +1,78 @@ +// SPDX-FileCopyrightText: 2022 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <variant> + +namespace Quotient { + +//! \brief A minimal subset of std::expected from C++23 +template <typename T, typename E, + std::enable_if_t<!std::is_same_v<T, E>, bool> = true> +class Expected { +private: + template <typename X> + using enable_if_constructible_t = std::enable_if_t< + std::is_constructible_v<T, X> || std::is_constructible_v<E, X>>; + +public: + using value_type = T; + using error_type = E; + + Expected() = default; + Expected(const Expected&) = default; + Expected(Expected&&) noexcept = default; + ~Expected() = default; + + template <typename X, typename = enable_if_constructible_t<X>> + QUO_IMPLICIT Expected(X&& x) // NOLINT(google-explicit-constructor) + : data(std::forward<X>(x)) + {} + + Expected& operator=(const Expected&) = default; + Expected& operator=(Expected&&) noexcept = default; + + template <typename X, typename = enable_if_constructible_t<X>> + Expected& operator=(X&& x) + { + data = std::forward<X>(x); + return *this; + } + + bool has_value() const { return std::holds_alternative<T>(data); } + explicit operator bool() const { return has_value(); } + + const value_type& value() const& { return std::get<T>(data); } + value_type& value() & { return std::get<T>(data); } + value_type value() && { return std::get<T>(std::move(data)); } + + const value_type& operator*() const& { return value(); } + value_type& operator*() & { return value(); } + + const value_type* operator->() const& { return std::get_if<T>(&data); } + value_type* operator->() & { return std::get_if<T>(&data); } + + template <class U> + T value_or(U&& fallback) const& + { + if (has_value()) + return value(); + return std::forward<U>(fallback); + } + template <class U> + T value_or(U&& fallback) && + { + if (has_value()) + return value(); + return std::forward<U>(fallback); + } + + const E& error() const& { return std::get<E>(data); } + E& error() & { return std::get<E>(data); } + +private: + std::variant<T, E> data; +}; + +} // namespace Quotient diff --git a/lib/function_traits.cpp b/lib/function_traits.cpp new file mode 100644 index 00000000..e3d27122 --- /dev/null +++ b/lib/function_traits.cpp @@ -0,0 +1,56 @@ +// 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; + +template <typename FnT> +using fn_return_t = typename function_traits<FnT>::return_type; + +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> +[[maybe_unused]] 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..143ed162 --- /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> + 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<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...>; +}; + +namespace _impl { + template <typename> + struct fn_object_traits; + + // Specialisation for a lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for a const lambda function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_object_traits<ReturnT (ClassT::*)(ArgTs...) const> + : function_traits<ReturnT(ArgTs...)> {}; + + // Specialisation for function objects with (non-overloaded) operator() + // (this includes non-generic lambdas) + template <typename T> + requires requires { &T::operator(); } + struct fn_traits<T> + : public fn_object_traits<decltype(&T::operator())> {}; + + // Specialisation for a member function in a non-functor class + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<ReturnT (ClassT::*)(ArgTs...)> + : function_traits<ReturnT(ClassT, ArgTs...)> {}; + + // Specialisation for a const member function + template <typename ReturnT, typename ClassT, typename... ArgTs> + struct fn_traits<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<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<ReturnT (ClassT::*)(ArgTs...) &&> + : function_traits<ReturnT(ClassT&&, ArgTs...)> {}; + + // Specialisation for a pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<ReturnT ClassT::*> + : function_traits<ReturnT&(ClassT)> {}; + + // Specialisation for a const pointer-to-member + template <typename ReturnT, typename ClassT> + struct fn_traits<const ReturnT ClassT::*> + : function_traits<const ReturnT&(ClassT)> {}; +} // namespace _impl + +template <typename FnT, int ArgN = 0> +using fn_arg_t = + std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; + +template <typename FnT> +constexpr auto fn_arg_count_v = + std::tuple_size_v<typename function_traits<FnT>::arg_types>; + +} // namespace Quotient diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 5960203d..da645a2d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -1,20 +1,6 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "basejob.h" @@ -22,15 +8,12 @@ #include <QtCore/QRegularExpression> #include <QtCore/QTimer> -#include <QtCore/QStringBuilder> #include <QtCore/QMetaEnum> #include <QtCore/QPointer> #include <QtNetwork/QNetworkAccessManager> #include <QtNetwork/QNetworkReply> #include <QtNetwork/QNetworkRequest> -#include <array> - using namespace Quotient; using std::chrono::seconds, std::chrono::milliseconds; using namespace std::chrono_literals; @@ -39,7 +22,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; @@ -47,19 +30,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; } @@ -77,12 +60,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 { @@ -92,8 +69,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) @@ -127,10 +104,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; @@ -161,9 +138,8 @@ public: QTimer timer; QTimer retryTimer; - static constexpr std::array<const JobTimeoutConfig, 3> errorStrategy { - { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } } - }; + static constexpr auto errorStrategy = std::to_array<const JobTimeoutConfig>( + { { 90s, 5s }, { 90s, 10s }, { 120s, 30s } }); int maxRetries = int(errorStrategy.size()); int retriesTaken = 0; @@ -175,10 +151,8 @@ public: [[nodiscard]] QString dumpRequest() const { - // FIXME: use std::array {} when Apple stdlib gets deduction guides for it - static const auto verbs = - make_array(QStringLiteral("GET"), QStringLiteral("PUT"), - QStringLiteral("POST"), QStringLiteral("DELETE")); + static const std::array verbs { "GET"_ls, "PUT"_ls, "POST"_ls, + "DELETE"_ls }; const auto verbWord = verbs.at(size_t(verb)); return verbWord % ' ' % (reply ? reply->url().toString(QUrl::RemoveQuery) @@ -187,14 +161,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(makeImpl<Private>(verb, std::move(endpoint), query, std::move(data), + needsToken)) { setObjectName(name); connect(&d->timer, &QTimer::timeout, this, &BaseJob::timeout); @@ -215,13 +211,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; @@ -238,16 +227,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 { @@ -264,7 +256,7 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) d->expectedContentTypes = contentTypes; } -const QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } +QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; } @@ -277,17 +269,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; } @@ -302,19 +294,14 @@ 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( -#if (QT_VERSION >= QT_VERSION_CHECK(5, 15, 0)) - QNetworkRequest::Http2AllowedAttribute -#else - QNetworkRequest::HTTP2AllowedAttribute -#endif // Qt doesn't combine HTTP2 with SSL quite right, occasionally crashing at // what seems like an attempt to write to a closed channel. If/when that // changes, false should be turned to true below. - , false); + req.setAttribute(QNetworkRequest::Http2AllowedAttribute, false); Q_ASSERT(req.url().isValid()); for (auto it = requestHeaders.cbegin(); it != requestHeaders.cend(); ++it) req.setRawHeader(it.key(), it.value()); @@ -367,7 +354,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); @@ -417,42 +404,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) @@ -517,7 +504,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) @@ -527,10 +514,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) @@ -540,16 +527,16 @@ BaseJob::Status BaseJob::prepareError() d->connection->limitRate(milliseconds(retryAfterMs)); - return { TooManyRequestsError, msg }; + return { TooManyRequests, msg }; } if (errCode == "M_CONSENT_NOT_GIVEN") { - d->errorUrl = errorJson.value("consent_uri"_ls).toString(); - return { UserConsentRequiredError }; + d->errorUrl = QUrl(errorJson.value("consent_uri"_ls).toString()); + 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()) @@ -562,9 +549,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) @@ -731,38 +718,41 @@ 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"); } } -int BaseJob::error() const { return d->status.code; } +int BaseJob::error() const { + return d->status.code; } -QString BaseJob::errorString() const { return d->status.message; } +QString BaseJob::errorString() const { + return d->status.message; } -QUrl BaseJob::errorUrl() const { return d->errorUrl; } +QUrl BaseJob::errorUrl() const { + return d->errorUrl; } void BaseJob::setStatus(Status s) { @@ -813,7 +803,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 be2926be..555c602b 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -1,28 +1,16 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "requestdata.h" -#include "../logging.h" -#include "../converters.h" +#include "logging.h" +#include "converters.h" // Common for csapi/ headers even though not used here +#include "quotient_common.h" // For DECL_DEPRECATED_ENUMERATOR #include <QtCore/QObject> +#include <QtCore/QStringBuilder> class QNetworkReply; class QSslError; @@ -32,12 +20,23 @@ class ConnectionData; enum class HttpVerb { Get, Put, Post, Delete }; -class BaseJob : public QObject { +class QUOTIENT_API BaseJob : public QObject { Q_OBJECT Q_PROPERTY(QUrl requestUrl READ requestUrl CONSTANT) Q_PROPERTY(int maxRetries READ maxRetries WRITE setMaxRetries) Q_PROPERTY(int statusCode READ error NOTIFY statusChanged) + + static QByteArray encodeIfParam(const QString& paramPart); + template <int N> + static 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() @@ -48,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, @@ -57,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, @@ -86,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,16 +123,25 @@ public: { return !operator==(other); } + bool operator==(int otherCode) const + { + return code == otherCode; + } + bool operator!=(int otherCode) const + { + return !operator==(otherCode); + } int code; QString message; }; 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; @@ -200,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); @@ -251,8 +247,8 @@ public: return dbg << j->objectName(); } -public slots: - void initiate(ConnectionData* connData, bool inBackground); +public Q_SLOTS: + void initiate(Quotient::ConnectionData* connData, bool inBackground); /** * Abandons the result of this job, arrived or unarrived. @@ -263,7 +259,7 @@ public slots: */ void abandon(); -signals: +Q_SIGNALS: /** The job is about to send a network request */ void aboutToSendRequest(); @@ -342,20 +338,22 @@ 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); - const QByteArrayList expectedKeys() const; + QByteArrayList expectedKeys() const; void addExpectedKey(const QByteArray &key); void setExpectedKeys(const QByteArrayList &keys); @@ -367,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 @@ -401,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 * @@ -433,7 +433,7 @@ protected: // Job objects should only be deleted via QObject::deleteLater ~BaseJob() override; -protected slots: +protected Q_SLOTS: void timeout(); /*! \brief Check the pending or received reply for upfront issues @@ -456,7 +456,7 @@ protected slots: */ virtual Status checkReply(const QNetworkReply *reply) const; -private slots: +private Q_SLOTS: void sendRequest(); void gotReply(); @@ -467,10 +467,10 @@ private: void finishJob(); class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; }; -inline bool isJobRunning(BaseJob* job) +inline bool QUOTIENT_API isJobPending(BaseJob* job) { return job && job->error() == BaseJob::Pending; } diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index 0011a97c..759d52c9 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -1,11 +1,19 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "downloadfilejob.h" #include <QtCore/QFile> #include <QtCore/QTemporaryFile> #include <QtNetwork/QNetworkReply> -using namespace Quotient; +#ifdef Quotient_E2EE_ENABLED +# include "events/filesourceinfo.h" +# include <QtCore/QCryptographicHash> +#endif + +using namespace Quotient; class DownloadFileJob::Private { public: Private() : tempFile(new QTemporaryFile()) {} @@ -17,6 +25,10 @@ public: QScopedPointer<QFile> targetFile; QScopedPointer<QFile> tempFile; + +#ifdef Quotient_E2EE_ENABLED + Omittable<EncryptedFileMetadata> encryptedFileMetadata; +#endif }; QUrl DownloadFileJob::makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri) @@ -29,11 +41,25 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename) : GetContentJob(serverName, mediaId) - , d(localFilename.isEmpty() ? new Private : new Private(localFilename)) + , d(localFilename.isEmpty() ? makeImpl<Private>() + : makeImpl<Private>(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); } +#ifdef Quotient_E2EE_ENABLED +DownloadFileJob::DownloadFileJob(const QString& serverName, + const QString& mediaId, + const EncryptedFileMetadata& file, + const QString& localFilename) + : GetContentJob(serverName, mediaId) + , d(localFilename.isEmpty() ? makeImpl<Private>() + : makeImpl<Private>(localFilename)) +{ + setObjectName(QStringLiteral("DownloadFileJob")); + d->encryptedFileMetadata = file; +} +#endif QString DownloadFileJob::targetFileName() const { return (d->targetFile ? d->targetFile : d->tempFile)->fileName(); @@ -48,7 +74,7 @@ void DownloadFileJob::doPrepare() setStatus(FileError, "Could not open the target file for writing"); return; } - if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::WriteOnly)) { + if (!d->tempFile->isReadable() && !d->tempFile->open(QIODevice::ReadWrite)) { qCWarning(JOBS) << "Couldn't open the temporary file" << d->tempFile->fileName() << "for writing"; setStatus(FileError, "Could not open the temporary download file"); @@ -93,21 +119,60 @@ void DownloadFileJob::beforeAbandon() d->tempFile->remove(); } +void decryptFile(QFile& sourceFile, const EncryptedFileMetadata& metadata, + QFile& targetFile) +{ + sourceFile.seek(0); + const auto encrypted = sourceFile.readAll(); // TODO: stream decryption + const auto decrypted = decryptFile(encrypted, metadata); + targetFile.write(decrypted); +} + BaseJob::Status DownloadFileJob::prepareResult() { if (d->targetFile) { - d->targetFile->close(); - if (!d->targetFile->remove()) { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; - return { FileError, "Couldn't finalise the download" }; +#ifdef Quotient_E2EE_ENABLED + if (d->encryptedFileMetadata.has_value()) { + decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile); + d->tempFile->remove(); + } else { +#endif + d->targetFile->close(); + if (!d->targetFile->remove()) { + qWarning(JOBS) << "Failed to remove the target file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!d->tempFile->rename(d->targetFile->fileName())) { + qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + << "to" << d->targetFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } +#ifdef Quotient_E2EE_ENABLED } - if (!d->tempFile->rename(d->targetFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() - << "to" << d->targetFile->fileName(); - return { FileError, "Couldn't finalise the download" }; +#endif + } else { +#ifdef Quotient_E2EE_ENABLED + if (d->encryptedFileMetadata.has_value()) { + QTemporaryFile tempTempFile; // Assuming it to be next to tempFile + decryptFile(*d->tempFile, *d->encryptedFileMetadata, tempTempFile); + d->tempFile->close(); + if (!d->tempFile->remove()) { + qWarning(JOBS) + << "Failed to remove the decrypted file placeholder"; + return { FileError, "Couldn't finalise the download" }; + } + if (!tempTempFile.rename(d->tempFile->fileName())) { + qWarning(JOBS) << "Failed to rename" << tempTempFile.fileName() + << "to" << d->tempFile->fileName(); + return { FileError, "Couldn't finalise the download" }; + } + } else { +#endif + d->tempFile->close(); +#ifdef Quotient_E2EE_ENABLED } - } else - d->tempFile->close(); - qCDebug(JOBS) << "Saved a file as" << targetFileName(); +#endif + } + qDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index e00fd9e4..cbbfd244 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -1,9 +1,14 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "csapi/content-repo.h" +#include "events/filesourceinfo.h" + namespace Quotient { -class DownloadFileJob : public GetContentJob { +class QUOTIENT_API DownloadFileJob : public GetContentJob { public: using GetContentJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri); @@ -11,11 +16,14 @@ public: DownloadFileJob(const QString& serverName, const QString& mediaId, const QString& localFilename = {}); +#ifdef Quotient_E2EE_ENABLED + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {}); +#endif QString targetFileName() const; private: class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; void doPrepare() override; void onSentRequest(QNetworkReply* reply) override; diff --git a/lib/jobs/mediathumbnailjob.cpp b/lib/jobs/mediathumbnailjob.cpp index a69f00e9..6fe8ef26 100644 --- a/lib/jobs/mediathumbnailjob.cpp +++ b/lib/jobs/mediathumbnailjob.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "mediathumbnailjob.h" @@ -32,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/mediathumbnailjob.h b/lib/jobs/mediathumbnailjob.h index e6d39085..c9f6da35 100644 --- a/lib/jobs/mediathumbnailjob.h +++ b/lib/jobs/mediathumbnailjob.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -23,7 +8,7 @@ #include <QtGui/QPixmap> namespace Quotient { -class MediaThumbnailJob : public GetContentThumbnailJob { +class QUOTIENT_API MediaThumbnailJob : public GetContentThumbnailJob { public: using GetContentThumbnailJob::makeRequestUrl; static QUrl makeRequestUrl(QUrl baseUrl, const QUrl& mxcUri, diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h deleted file mode 100644 index 5a4d942c..00000000 --- a/lib/jobs/postreadmarkersjob.h +++ /dev/null @@ -1,38 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "basejob.h" - -#include <QtCore/QJsonObject> - -using namespace Quotient; - -class PostReadMarkersJob : public BaseJob { -public: - explicit PostReadMarkersJob(const QString& roomId, - const QString& readUpToEventId) - : BaseJob( - HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId)) - { - setRequestData( - QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } }); - } -}; diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index cec15954..ab249f6d 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -1,5 +1,9 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "requestdata.h" +#include <QtCore/QIODevice> #include <QtCore/QBuffer> #include <QtCore/QByteArray> #include <QtCore/QJsonArray> @@ -10,7 +14,7 @@ using namespace Quotient; auto fromData(const QByteArray& data) { - auto source = std::make_unique<QBuffer>(); + auto source = makeImpl<QBuffer, QIODevice>(); source->setData(data); source->open(QIODevice::ReadOnly); return source; @@ -28,4 +32,6 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} -RequestData::~RequestData() = default; +RequestData::RequestData(QIODevice* source) + : _source(acquireImpl(source)) +{} diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 9cb5ecaf..accc8f71 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -1,26 +1,9 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include <QtCore/QByteArray> - -#include <memory> +#include "util.h" class QJsonObject; class QJsonArray; @@ -34,22 +17,19 @@ namespace Quotient { * as well as JSON (and possibly other structures in the future) to * a QByteArray consumed by QNetworkAccessManager request methods. */ -class RequestData { +class QUOTIENT_API RequestData { public: - RequestData(const QByteArray& a = {}); - RequestData(const QJsonObject& jo); - RequestData(const QJsonArray& ja); - RequestData(QIODevice* source) : _source(std::unique_ptr<QIODevice>(source)) - {} - RequestData(RequestData&&) = default; - RequestData& operator=(RequestData&&) = default; - ~RequestData(); + // NOLINTBEGIN(google-explicit-constructor): that check should learn about + // explicit(false) + QUO_IMPLICIT RequestData(const QByteArray& a = {}); + QUO_IMPLICIT RequestData(const QJsonObject& jo); + QUO_IMPLICIT RequestData(const QJsonArray& ja); + QUO_IMPLICIT RequestData(QIODevice* source); + // NOLINTEND(google-explicit-constructor) QIODevice* source() const { return _source.get(); } private: - std::unique_ptr<QIODevice> _source; + ImplPtr<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 9087fe50..f5c632bf 100644 --- a/lib/jobs/syncjob.cpp +++ b/lib/jobs/syncjob.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "syncjob.h" @@ -25,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; @@ -52,10 +37,12 @@ SyncJob::SyncJob(const QString& since, const Filter& filter, int timeout, BaseJob::Status SyncJob::prepareResult() { d.parseJson(jsonData()); - if (d.unresolvedRooms().isEmpty()) + if (Q_LIKELY(d.unresolvedRooms().isEmpty())) return Success; - qCCritical(MAIN).noquote() << "Incomplete sync response, missing rooms:" + Q_ASSERT(d.unresolvedRooms().isEmpty()); + qCCritical(MAIN).noquote() << "Rooms missing after processing sync " + "response, possibly a bug in SyncData: " << d.unresolvedRooms().join(','); return IncorrectResponse; } diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index bf139a7b..b7bfbbb3 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2016 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -30,7 +15,7 @@ public: explicit SyncJob(const QString& since, const Filter& filter, int timeout = -1, const QString& presence = {}); - SyncData&& takeData() { return std::move(d); } + SyncData takeData() { return std::move(d); } protected: Status prepareResult() override; diff --git a/lib/joinstate.h b/lib/joinstate.h deleted file mode 100644 index 31c2b6a7..00000000 --- a/lib/joinstate.h +++ /dev/null @@ -1,47 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include <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/keyverificationsession.cpp b/lib/keyverificationsession.cpp new file mode 100644 index 00000000..4c61964c --- /dev/null +++ b/lib/keyverificationsession.cpp @@ -0,0 +1,501 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "keyverificationsession.h" + +#include "connection.h" +#include "database.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolmutils.h" +#include "olm/sas.h" + +#include "events/event.h" + +#include <QtCore/QCryptographicHash> +#include <QtCore/QTimer> +#include <QtCore/QUuid> + +#include <chrono> + +using namespace Quotient; +using namespace std::chrono; + +const QStringList supportedMethods = { SasV1Method }; + +QStringList commonSupportedMethods(const QStringList& remoteMethods) +{ + QStringList result; + for (const auto& method : remoteMethods) { + if (supportedMethods.contains(method)) { + result += method; + } + } + return result; +} + +KeyVerificationSession::KeyVerificationSession( + QString remoteUserId, const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted) + : QObject(connection) + , m_remoteUserId(std::move(remoteUserId)) + , m_remoteDeviceId(event.fromDevice()) + , m_transactionId(event.transactionId()) + , m_connection(connection) + , m_encrypted(encrypted) + , m_remoteSupportedMethods(event.methods()) +{ + const auto& currentTime = QDateTime::currentDateTime(); + const auto timeoutTime = + std::min(event.timestamp().addSecs(600), currentTime.addSecs(120)); + const milliseconds timeout{ currentTime.msecsTo(timeoutTime) }; + if (timeout > 5s) + init(timeout); + // Otherwise don't even bother starting up +} + +KeyVerificationSession::KeyVerificationSession(QString userId, QString deviceId, + Connection* connection) + : QObject(connection) + , m_remoteUserId(std::move(userId)) + , m_remoteDeviceId(std::move(deviceId)) + , m_transactionId(QUuid::createUuid().toString()) + , m_connection(connection) + , m_encrypted(false) +{ + init(600s); + QMetaObject::invokeMethod(this, &KeyVerificationSession::sendRequest); +} + +void KeyVerificationSession::init(milliseconds timeout) +{ + QTimer::singleShot(timeout, this, [this] { cancelVerification(TIMEOUT); }); + + m_sas = olm_sas(new std::byte[olm_sas_size()]); + const auto randomLength = olm_create_sas_random_length(m_sas); + olm_create_sas(m_sas, RandomBuffer(randomLength), randomLength); +} + +KeyVerificationSession::~KeyVerificationSession() +{ + olm_clear_sas(m_sas); + delete[] reinterpret_cast<std::byte*>(m_sas); +} + +void KeyVerificationSession::handleEvent(const KeyVerificationEvent& baseEvent) +{ + if (!switchOnType( + baseEvent, + [this](const KeyVerificationCancelEvent& event) { + setError(stringToError(event.code())); + setState(CANCELED); + return true; + }, + [this](const KeyVerificationStartEvent& event) { + if (state() != WAITINGFORREADY && state() != READY) + return false; + handleStart(event); + return true; + }, + [this](const KeyVerificationReadyEvent& event) { + if (state() == WAITINGFORREADY) + handleReady(event); + // ACCEPTED is also fine here because it's possible to receive + // ready and start in the same sync, in which case start might + // be handled before ready. + return state() == WAITINGFORREADY || state() == ACCEPTED; + }, + [this](const KeyVerificationAcceptEvent& event) { + if (state() != WAITINGFORACCEPT) + return false; + m_commitment = event.commitment(); + sendKey(); + setState(WAITINGFORKEY); + return true; + }, + [this](const KeyVerificationKeyEvent& event) { + if (state() != ACCEPTED && state() != WAITINGFORKEY) + return false; + handleKey(event); + return true; + }, + [this](const KeyVerificationMacEvent& event) { + if (state() != WAITINGFORMAC && state() != WAITINGFORVERIFICATION) + return false; + handleMac(event); + return true; + }, + [this](const KeyVerificationDoneEvent&) { return state() == DONE; })) + cancelVerification(UNEXPECTED_MESSAGE); +} + +struct EmojiStoreEntry : EmojiEntry { + QHash<QString, QString> translatedDescriptions; + + explicit EmojiStoreEntry(const QJsonObject& json) + : EmojiEntry{ fromJson<QString>(json["emoji"]), + fromJson<QString>(json["description"]) } + , translatedDescriptions{ fromJson<QHash<QString, QString>>( + json["translated_descriptions"]) } + {} +}; + +using EmojiStore = QVector<EmojiStoreEntry>; + +EmojiStore loadEmojiStore() +{ + QFile dataFile(":/sas-emoji.json"); + dataFile.open(QFile::ReadOnly); + return fromJson<EmojiStore>( + QJsonDocument::fromJson(dataFile.readAll()).array()); +} + +EmojiEntry emojiForCode(int code, const QString& language) +{ + static const EmojiStore emojiStore = loadEmojiStore(); + const auto& entry = emojiStore[code]; + if (!language.isEmpty()) + if (const auto translatedDescription = + emojiStore[code].translatedDescriptions.value(language); + !translatedDescription.isNull()) + return { entry.emoji, translatedDescription }; + + return SLICE(entry, EmojiEntry); +} + +void KeyVerificationSession::handleKey(const KeyVerificationKeyEvent& event) +{ + auto eventKey = event.key().toLatin1(); + olm_sas_set_their_key(m_sas, eventKey.data(), eventKey.size()); + + if (startSentByUs) { + const auto paddedCommitment = + QCryptographicHash::hash((event.key() % m_startEvent).toLatin1(), + QCryptographicHash::Sha256) + .toBase64(); + const QLatin1String unpaddedCommitment(paddedCommitment.constData(), + paddedCommitment.indexOf('=')); + if (unpaddedCommitment != m_commitment) { + qCWarning(E2EE) << "Commitment mismatch; aborting verification"; + cancelVerification(MISMATCHED_COMMITMENT); + return; + } + } else { + sendKey(); + } + + std::string key(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, key.data(), key.size()); + + std::array<std::byte, 6> output{}; + const auto infoTemplate = + startSentByUs ? "MATRIX_KEY_VERIFICATION_SAS|%1|%2|%3|%4|%5|%6|%7"_ls + : "MATRIX_KEY_VERIFICATION_SAS|%4|%5|%6|%1|%2|%3|%7"_ls; + + const auto info = infoTemplate + .arg(m_connection->userId(), m_connection->deviceId(), + key.data(), m_remoteUserId, m_remoteDeviceId, + event.key(), m_transactionId) + .toLatin1(); + olm_sas_generate_bytes(m_sas, info.data(), info.size(), output.data(), + output.size()); + + static constexpr auto x3f = std::byte{ 0x3f }; + const std::array<std::byte, 7> code{ + output[0] >> 2, + (output[0] << 4 & x3f) | output[1] >> 4, + (output[1] << 2 & x3f) | output[2] >> 6, + output[2] & x3f, + output[3] >> 2, + (output[3] << 4 & x3f) | output[4] >> 4, + (output[4] << 2 & x3f) | output[5] >> 6 + }; + + const auto uiLanguages = QLocale().uiLanguages(); + const auto preferredLanguage = uiLanguages.isEmpty() + ? QString() + : uiLanguages.front().section('-', 0, 0); + for (const auto& c : code) + m_sasEmojis += emojiForCode(std::to_integer<int>(c), preferredLanguage); + + emit sasEmojisChanged(); + emit keyReceived(); + setState(WAITINGFORVERIFICATION); +} + +QString KeyVerificationSession::calculateMac(const QString& input, + bool verifying, + const QString& keyId) +{ + QByteArray inputBytes = input.toLatin1(); + QByteArray outputBytes(olm_sas_mac_length(m_sas), '\0'); + const auto macInfo = + (verifying ? "MATRIX_KEY_VERIFICATION_MAC%3%4%1%2%5%6"_ls + : "MATRIX_KEY_VERIFICATION_MAC%1%2%3%4%5%6"_ls) + .arg(m_connection->userId(), m_connection->deviceId(), + m_remoteUserId, m_remoteDeviceId, m_transactionId, keyId) + .toLatin1(); + olm_sas_calculate_mac(m_sas, inputBytes.data(), inputBytes.size(), + macInfo.data(), macInfo.size(), outputBytes.data(), + outputBytes.size()); + return QString::fromLatin1(outputBytes.data(), outputBytes.indexOf('=')); +} + +void KeyVerificationSession::sendMac() +{ + QString edKeyId = "ed25519:" % m_connection->deviceId(); + + auto keys = calculateMac(edKeyId, false); + + QJsonObject mac; + auto key = m_connection->olmAccount()->deviceKeys().keys[edKeyId]; + mac[edKeyId] = calculateMac(key, false, edKeyId); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationMacEvent(m_transactionId, keys, + mac), + m_encrypted); + setState (macReceived ? DONE : WAITINGFORMAC); + m_verified = true; + if (!m_pendingEdKeyId.isEmpty()) { + trustKeys(); + } +} + +void KeyVerificationSession::sendDone() +{ + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationDoneEvent(m_transactionId), + m_encrypted); +} + +void KeyVerificationSession::sendKey() +{ + QByteArray keyBytes(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, keyBytes.data(), keyBytes.size()); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationKeyEvent(m_transactionId, + keyBytes), + m_encrypted); +} + + +void KeyVerificationSession::cancelVerification(Error error) +{ + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationCancelEvent(m_transactionId, + errorToString(error)), + m_encrypted); + setState(CANCELED); + setError(error); + emit finished(); + deleteLater(); +} + +void KeyVerificationSession::sendReady() +{ + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) { + cancelVerification(UNKNOWN_METHOD); + return; + } + + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationReadyEvent(m_transactionId, m_connection->deviceId(), + methods), + m_encrypted); + setState(READY); + + if (methods.size() == 1) { + sendStartSas(); + } +} + +void KeyVerificationSession::sendStartSas() +{ + startSentByUs = true; + KeyVerificationStartEvent event(m_transactionId, m_connection->deviceId()); + m_startEvent = + QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact); + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, event, + m_encrypted); + setState(WAITINGFORACCEPT); +} + +void KeyVerificationSession::handleReady(const KeyVerificationReadyEvent& event) +{ + setState(READY); + m_remoteSupportedMethods = event.methods(); + auto methods = commonSupportedMethods(m_remoteSupportedMethods); + + if (methods.isEmpty()) + cancelVerification(UNKNOWN_METHOD); + else if (methods.size() == 1) + sendStartSas(); // -> WAITINGFORACCEPT +} + +void KeyVerificationSession::handleStart(const KeyVerificationStartEvent& event) +{ + if (startSentByUs) { + if (m_remoteUserId > m_connection->userId() || (m_remoteUserId == m_connection->userId() && m_remoteDeviceId > m_connection->deviceId())) { + return; + } else { + startSentByUs = false; + } + } + QByteArray publicKey(olm_sas_pubkey_length(m_sas), '\0'); + olm_sas_get_pubkey(m_sas, publicKey.data(), publicKey.size()); + const auto canonicalEvent = QString(QJsonDocument(event.contentJson()).toJson(QJsonDocument::Compact)); + auto commitment = QString(QCryptographicHash::hash((QString(publicKey) % canonicalEvent).toLatin1(), QCryptographicHash::Sha256).toBase64()); + commitment = commitment.left(commitment.indexOf('=')); + + m_connection->sendToDevice(m_remoteUserId, m_remoteDeviceId, + KeyVerificationAcceptEvent(m_transactionId, + commitment), + m_encrypted); + setState(ACCEPTED); +} + +void KeyVerificationSession::handleMac(const KeyVerificationMacEvent& event) +{ + QStringList keys = event.mac().keys(); + keys.sort(); + const auto& key = keys.join(","); + const QString edKeyId = "ed25519:"_ls % m_remoteDeviceId; + + if (calculateMac(m_connection->edKeyForUserDevice(m_remoteUserId, m_remoteDeviceId), true, edKeyId) != event.mac()[edKeyId]) { + cancelVerification(KEY_MISMATCH); + return; + } + + if (calculateMac(key, true) != event.keys()) { + cancelVerification(KEY_MISMATCH); + return; + } + + m_pendingEdKeyId = edKeyId; + + if (m_verified) { + trustKeys(); + } +} + +void KeyVerificationSession::trustKeys() +{ + m_connection->database()->setSessionVerified(m_pendingEdKeyId); + emit m_connection->sessionVerified(m_remoteUserId, m_remoteDeviceId); + macReceived = true; + + if (state() == WAITINGFORMAC) { + setState(DONE); + sendDone(); + emit finished(); + deleteLater(); + } +} + +QVector<EmojiEntry> KeyVerificationSession::sasEmojis() const +{ + return m_sasEmojis; +} + +void KeyVerificationSession::sendRequest() +{ + m_connection->sendToDevice( + m_remoteUserId, m_remoteDeviceId, + KeyVerificationRequestEvent(m_transactionId, m_connection->deviceId(), + supportedMethods, + QDateTime::currentDateTime()), + m_encrypted); + setState(WAITINGFORREADY); +} + +KeyVerificationSession::State KeyVerificationSession::state() const +{ + return m_state; +} + +void KeyVerificationSession::setState(KeyVerificationSession::State state) +{ + m_state = state; + emit stateChanged(); +} + +KeyVerificationSession::Error KeyVerificationSession::error() const +{ + return m_error; +} + +void KeyVerificationSession::setError(Error error) +{ + m_error = error; + emit errorChanged(); +} + +QString KeyVerificationSession::errorToString(Error error) +{ + switch(error) { + case NONE: + return "none"_ls; + case TIMEOUT: + return "m.timeout"_ls; + case USER: + return "m.user"_ls; + case UNEXPECTED_MESSAGE: + return "m.unexpected_message"_ls; + case UNKNOWN_TRANSACTION: + return "m.unknown_transaction"_ls; + case UNKNOWN_METHOD: + return "m.unknown_method"_ls; + case KEY_MISMATCH: + return "m.key_mismatch"_ls; + case USER_MISMATCH: + return "m.user_mismatch"_ls; + case INVALID_MESSAGE: + return "m.invalid_message"_ls; + case SESSION_ACCEPTED: + return "m.accepted"_ls; + case MISMATCHED_COMMITMENT: + return "m.mismatched_commitment"_ls; + case MISMATCHED_SAS: + return "m.mismatched_sas"_ls; + default: + return "m.user"_ls; + } +} + +KeyVerificationSession::Error KeyVerificationSession::stringToError(const QString& error) +{ + if (error == "m.timeout"_ls) { + return REMOTE_TIMEOUT; + } else if (error == "m.user"_ls) { + return REMOTE_USER; + } else if (error == "m.unexpected_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_message"_ls) { + return REMOTE_UNEXPECTED_MESSAGE; + } else if (error == "m.unknown_transaction"_ls) { + return REMOTE_UNKNOWN_TRANSACTION; + } else if (error == "m.unknown_method"_ls) { + return REMOTE_UNKNOWN_METHOD; + } else if (error == "m.key_mismatch"_ls) { + return REMOTE_KEY_MISMATCH; + } else if (error == "m.user_mismatch"_ls) { + return REMOTE_USER_MISMATCH; + } else if (error == "m.invalid_message"_ls) { + return REMOTE_INVALID_MESSAGE; + } else if (error == "m.accepted"_ls) { + return REMOTE_SESSION_ACCEPTED; + } else if (error == "m.mismatched_commitment"_ls) { + return REMOTE_MISMATCHED_COMMITMENT; + } else if (error == "m.mismatched_sas"_ls) { + return REMOTE_MISMATCHED_SAS; + } + return NONE; +} + +QString KeyVerificationSession::remoteDeviceId() const +{ + return m_remoteDeviceId; +} diff --git a/lib/keyverificationsession.h b/lib/keyverificationsession.h new file mode 100644 index 00000000..32a91cfc --- /dev/null +++ b/lib/keyverificationsession.h @@ -0,0 +1,153 @@ +// SPDX-FileCopyrightText: 2022 Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/keyverificationevent.h" + +#include <QtCore/QObject> + +struct OlmSAS; + +namespace Quotient { +class Connection; + +struct QUOTIENT_API EmojiEntry { + QString emoji; + QString description; + + Q_GADGET + Q_PROPERTY(QString emoji MEMBER emoji CONSTANT) + Q_PROPERTY(QString description MEMBER description CONSTANT) + +public: + bool operator==(const EmojiEntry& rhs) const = default; +}; + +/** A key verification session. Listen for incoming sessions by connecting to Connection::newKeyVerificationSession. + Start a new session using Connection::startKeyVerificationSession. + The object is delete after finished is emitted. +*/ +class QUOTIENT_API KeyVerificationSession : public QObject +{ + Q_OBJECT + +public: + enum State { + INCOMING, ///< There is a request for verification incoming + //! We sent a request for verification and are waiting for ready + WAITINGFORREADY, + //! Either party sent a ready as a response to a request; the user + //! selects a method + READY, + WAITINGFORACCEPT, ///< We sent a start and are waiting for an accept + ACCEPTED, ///< The other party sent an accept and is waiting for a key + WAITINGFORKEY, ///< We're waiting for a key + //! We're waiting for the *user* to verify the emojis + WAITINGFORVERIFICATION, + WAITINGFORMAC, ///< We're waiting for the mac + CANCELED, ///< The session has been canceled + DONE, ///< The verification is done + }; + Q_ENUM(State) + + enum Error { + NONE, + TIMEOUT, + REMOTE_TIMEOUT, + USER, + REMOTE_USER, + UNEXPECTED_MESSAGE, + REMOTE_UNEXPECTED_MESSAGE, + UNKNOWN_TRANSACTION, + REMOTE_UNKNOWN_TRANSACTION, + UNKNOWN_METHOD, + REMOTE_UNKNOWN_METHOD, + KEY_MISMATCH, + REMOTE_KEY_MISMATCH, + USER_MISMATCH, + REMOTE_USER_MISMATCH, + INVALID_MESSAGE, + REMOTE_INVALID_MESSAGE, + SESSION_ACCEPTED, + REMOTE_SESSION_ACCEPTED, + MISMATCHED_COMMITMENT, + REMOTE_MISMATCHED_COMMITMENT, + MISMATCHED_SAS, + REMOTE_MISMATCHED_SAS, + }; + Q_ENUM(Error) + + Q_PROPERTY(QString remoteDeviceId MEMBER m_remoteDeviceId CONSTANT) + Q_PROPERTY(QVector<EmojiEntry> sasEmojis READ sasEmojis NOTIFY sasEmojisChanged) + Q_PROPERTY(State state READ state NOTIFY stateChanged) + Q_PROPERTY(Error error READ error NOTIFY errorChanged) + + KeyVerificationSession(QString remoteUserId, + const KeyVerificationRequestEvent& event, + Connection* connection, bool encrypted); + KeyVerificationSession(QString userId, QString deviceId, + Connection* connection); + ~KeyVerificationSession() override; + Q_DISABLE_COPY_MOVE(KeyVerificationSession) + + void handleEvent(const KeyVerificationEvent& baseEvent); + + QVector<EmojiEntry> sasEmojis() const; + State state() const; + + Error error() const; + + QString remoteDeviceId() const; + +public Q_SLOTS: + void sendRequest(); + void sendReady(); + void sendMac(); + void sendStartSas(); + void sendKey(); + void sendDone(); + void cancelVerification(Error error); + +Q_SIGNALS: + void keyReceived(); + void sasEmojisChanged(); + void stateChanged(); + void errorChanged(); + void finished(); + +private: + const QString m_remoteUserId; + const QString m_remoteDeviceId; + const QString m_transactionId; + Connection* m_connection; + OlmSAS* m_sas = nullptr; + QVector<EmojiEntry> m_sasEmojis; + bool startSentByUs = false; + State m_state = INCOMING; + Error m_error = NONE; + QString m_startEvent; + QString m_commitment; + bool macReceived = false; + bool m_encrypted; + QStringList m_remoteSupportedMethods; + bool m_verified = false; + QString m_pendingEdKeyId{}; + + void handleReady(const KeyVerificationReadyEvent& event); + void handleStart(const KeyVerificationStartEvent& event); + void handleKey(const KeyVerificationKeyEvent& event); + void handleMac(const KeyVerificationMacEvent& event); + void init(std::chrono::milliseconds timeout); + void setState(State state); + void setError(Error error); + static QString errorToString(Error error); + static Error stringToError(const QString& error); + void trustKeys(); + + QByteArray macInfo(bool verifying, const QString& key = "KEY_IDS"_ls); + QString calculateMac(const QString& input, bool verifying, const QString& keyId= "KEY_IDS"_ls); +}; + +} // namespace Quotient +Q_DECLARE_METATYPE(Quotient::EmojiEntry) diff --git a/lib/logging.cpp b/lib/logging.cpp index c346fbf1..460caced 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -1,20 +1,6 @@ -/****************************************************************************** - * Copyright (C) 2017 Elvis Angelaccio <elvid.angelaccio@kde.org> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Elvis Angelaccio <elvid.angelaccio@kde.org> +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "logging.h" @@ -24,9 +10,13 @@ LOGGING_CATEGORY(MAIN, "quotient.main") LOGGING_CATEGORY(EVENTS, "quotient.events") LOGGING_CATEGORY(STATE, "quotient.events.state") +LOGGING_CATEGORY(MEMBERS, "quotient.events.members") LOGGING_CATEGORY(MESSAGES, "quotient.events.messages") LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") LOGGING_CATEGORY(E2EE, "quotient.e2ee") LOGGING_CATEGORY(JOBS, "quotient.jobs") LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") +LOGGING_CATEGORY(THUMBNAILJOB, "quotient.jobs.thumbnail") +LOGGING_CATEGORY(NETWORK, "quotient.network") LOGGING_CATEGORY(PROFILER, "quotient.profiler") +LOGGING_CATEGORY(DATABASE, "quotient.database") diff --git a/lib/logging.h b/lib/logging.h index ce4131bb..1fafa04b 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -1,20 +1,6 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Elvis Angelaccio <elvid.angelaccio@kde.org> +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -23,13 +9,17 @@ Q_DECLARE_LOGGING_CATEGORY(MAIN) Q_DECLARE_LOGGING_CATEGORY(STATE) +Q_DECLARE_LOGGING_CATEGORY(MEMBERS) Q_DECLARE_LOGGING_CATEGORY(MESSAGES) Q_DECLARE_LOGGING_CATEGORY(EVENTS) Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) Q_DECLARE_LOGGING_CATEGORY(E2EE) Q_DECLARE_LOGGING_CATEGORY(JOBS) Q_DECLARE_LOGGING_CATEGORY(SYNCJOB) +Q_DECLARE_LOGGING_CATEGORY(THUMBNAILJOB) +Q_DECLARE_LOGGING_CATEGORY(NETWORK) Q_DECLARE_LOGGING_CATEGORY(PROFILER) +Q_DECLARE_LOGGING_CATEGORY(DATABASE) namespace Quotient { // QDebug manipulators @@ -48,24 +38,13 @@ 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 dbg.verbosity(QDebug::MinimumVerbosity); } inline qint64 profilerMinNsecs() @@ -79,15 +58,24 @@ inline qint64 profilerMinNsecs() * 1000; } } // namespace Quotient -/// \deprecated Use namespace Quotient instead -namespace QMatrixClient = Quotient; -inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) +/** + * @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); // NOLINT(performance-unnecessary-value-param) +} + +inline QDebug operator<<(QDebug debug_object, QElapsedTimer et) { - auto val = et.nsecsElapsed() / 1000; - if (val < 1000) - debug_object << val << "µs"; - else - debug_object << val / 1000 << "ms"; + // NOLINTNEXTLINE(bugprone-integer-division) + debug_object << static_cast<double>(et.nsecsElapsed() / 1000) / 1000 + << "ms"; // Show in ms with 3 decimal digits precision return debug_object; } diff --git a/lib/mxcreply.cpp b/lib/mxcreply.cpp new file mode 100644 index 00000000..ce833b98 --- /dev/null +++ b/lib/mxcreply.cpp @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "mxcreply.h" + +#include <QtCore/QBuffer> +#include "accountregistry.h" +#include "room.h" + +#ifdef Quotient_E2EE_ENABLED +#include "events/filesourceinfo.h" +#endif + +using namespace Quotient; + +class MxcReply::Private +{ +public: + explicit Private(QNetworkReply* r = nullptr) + : m_reply(r) + {} + QNetworkReply* m_reply; + Omittable<EncryptedFileMetadata> m_encryptedFile; + QIODevice* m_device = nullptr; +}; + +MxcReply::MxcReply(QNetworkReply* reply) + : d(makeImpl<Private>(reply)) +{ + d->m_device = d->m_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(makeImpl<Private>(reply)) +{ + reply->setParent(this); + connect(d->m_reply, &QNetworkReply::finished, this, [this]() { + setError(d->m_reply->error(), d->m_reply->errorString()); + +#ifdef Quotient_E2EE_ENABLED + if(!d->m_encryptedFile.has_value()) { + d->m_device = d->m_reply; + } else { + auto buffer = new QBuffer(this); + buffer->setData( + decryptFile(d->m_reply->readAll(), *d->m_encryptedFile)); + buffer->open(ReadOnly); + d->m_device = buffer; + } +#else + d->m_device = d->m_reply; +#endif + setOpenMode(ReadOnly); + emit finished(); + }); + +#ifdef Quotient_E2EE_ENABLED + auto eventIt = room->findInTimeline(eventId); + if(eventIt != room->historyEdge()) { + if (auto event = eventIt->viewAs<RoomMessageEvent>()) { + if (auto* efm = std::get_if<EncryptedFileMetadata>( + &event->content()->fileInfo()->source)) + d->m_encryptedFile = *efm; + } + } +#endif +} + +MxcReply::MxcReply() + : d(ZeroImpl<Private>()) +{ + 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 errorOccurred(QNetworkReply::ProtocolInvalidOperationError); + emit finished(); + }, Qt::QueuedConnection); +} + +qint64 MxcReply::readData(char *data, qint64 maxSize) +{ + return d->m_device->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..f6c4a34d --- /dev/null +++ b/lib/mxcreply.h @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Tobias Fella <fella@posteo.de> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "util.h" + +#include <QtNetwork/QNetworkReply> + +namespace Quotient { +class Room; + +class QUOTIENT_API MxcReply : public QNetworkReply +{ + Q_OBJECT +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; + ImplPtr<Private> d; +}; +} diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index e8aa85df..44a306d1 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -1,35 +1,45 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "networkaccessmanager.h" +#include "connection.h" +#include "room.h" +#include "accountregistry.h" +#include "mxcreply.h" + #include <QtCore/QCoreApplication> +#include <QtCore/QThread> +#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(makeImpl<Private>(this)) {} QList<QSslError> NetworkAccessManager::ignoredSslErrors() const @@ -37,6 +47,16 @@ QList<QSslError> NetworkAccessManager::ignoredSslErrors() const return d->ignoredSslErrors; } +void NetworkAccessManager::ignoreSslErrors(bool ignore) const +{ + if (ignore) { + connect(this, &QNetworkAccessManager::sslErrors, this, + [](QNetworkReply* reply) { reply->ignoreSslErrors(); }); + } else { + disconnect(this, &QNetworkAccessManager::sslErrors, this, nullptr); + } +} + void NetworkAccessManager::addIgnoredSslError(const QSslError& error) { d->ignoredSslErrors << error; @@ -47,31 +67,63 @@ void NetworkAccessManager::clearIgnoredSslErrors() d->ignoredSslErrors.clear(); } -static NetworkAccessManager* createNam() -{ - auto nam = new NetworkAccessManager(QCoreApplication::instance()); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - // See #109; in newer Qt, bearer management is deprecated altogether - NetworkAccessManager::connect(nam, - &QNetworkAccessManager::networkAccessibleChanged, [nam] { - nam->setNetworkAccessible(QNetworkAccessManager::Accessible); - }); -#endif - return nam; -} - NetworkAccessManager* NetworkAccessManager::instance() { - static auto* nam = createNam(); + thread_local auto* nam = [] { + auto* namInit = new NetworkAccessManager(); + connect(QThread::currentThread(), &QThread::finished, namInit, + &QObject::deleteLater); + return namInit; + }(); return nam; } -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 = Accounts.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 a678b80f..01b0599d 100644 --- a/lib/networkaccessmanager.h +++ b/lib/networkaccessmanager.h @@ -1,46 +1,35 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include <QtNetwork/QNetworkAccessManager> +#include "util.h" -#include <memory> +#include <QtNetwork/QNetworkAccessManager> namespace Quotient { -class NetworkAccessManager : public QNetworkAccessManager { + +class QUOTIENT_API NetworkAccessManager : public QNetworkAccessManager { Q_OBJECT public: NetworkAccessManager(QObject* parent = nullptr); - ~NetworkAccessManager() override; QList<QSslError> ignoredSslErrors() const; void addIgnoredSslError(const QSslError& error); void clearIgnoredSslErrors(); + void ignoreSslErrors(bool ignore = true) const; /** 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; class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient diff --git a/lib/networksettings.cpp b/lib/networksettings.cpp index 40ecba11..06b1fdf9 100644 --- a/lib/networksettings.cpp +++ b/lib/networksettings.cpp @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "networksettings.h" @@ -26,9 +11,9 @@ void NetworkSettings::setupApplicationProxy() const { proxyType(), proxyHostName(), proxyPort() }); } -QTNT_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, +QUO_DEFINE_SETTING(NetworkSettings, QNetworkProxy::ProxyType, proxyType, "proxy_type", QNetworkProxy::DefaultProxy, setProxyType) -QTNT_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", +QUO_DEFINE_SETTING(NetworkSettings, QString, proxyHostName, "proxy_hostname", {}, setProxyHostName) -QTNT_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, +QUO_DEFINE_SETTING(NetworkSettings, quint16, proxyPort, "proxy_port", -1, setProxyPort) diff --git a/lib/networksettings.h b/lib/networksettings.h index 2399cf5f..44247e59 100644 --- a/lib/networksettings.h +++ b/lib/networksettings.h @@ -1,20 +1,5 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once @@ -25,11 +10,11 @@ Q_DECLARE_METATYPE(QNetworkProxy::ProxyType) namespace Quotient { -class NetworkSettings : public SettingsGroup { +class QUOTIENT_API NetworkSettings : public SettingsGroup { Q_OBJECT - QTNT_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) - QTNT_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) - QTNT_DECLARE_SETTING(quint16, proxyPort, setProxyPort) + QUO_DECLARE_SETTING(QNetworkProxy::ProxyType, proxyType, setProxyType) + QUO_DECLARE_SETTING(QString, proxyHostName, setProxyHostName) + QUO_DECLARE_SETTING(quint16, proxyPort, setProxyPort) Q_PROPERTY(QString proxyHost READ proxyHostName WRITE setProxyHostName) public: template <typename... ArgTs> diff --git a/lib/omittable.h b/lib/omittable.h new file mode 100644 index 00000000..0718aaff --- /dev/null +++ b/lib/omittable.h @@ -0,0 +1,217 @@ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <optional> +#include <functional> + +namespace Quotient { + +template <typename T> +class Omittable; + +constexpr auto none = std::nullopt; + +//! \brief Lift an operation into dereferenceable types (Omittables or pointers) +//! +//! This is a more generic version of Omittable::then() that extends to +//! an arbitrary number of arguments of any type that is dereferenceable (unary +//! operator*() can be applied to it) and (explicitly or implicitly) convertible +//! to bool. This allows to streamline checking for nullptr/none before applying +//! the operation on the underlying types. \p fn is only invoked if all \p args +//! are "truthy" (i.e. <tt>(... && bool(args)) == true</tt>). +//! \param fn A callable that should accept the types stored inside +//! Omittables/pointers passed in \p args +//! \return Always an Omittable: if \p fn returns another type, lift() wraps +//! it in an Omittable; if \p fn returns an Omittable, that return value +//! (or none) is returned as is. +template <typename FnT, typename... ArgTs> +inline auto lift(FnT&& fn, ArgTs&&... args) +{ + if constexpr (std::is_void_v<decltype(std::invoke(std::forward<FnT>(fn), + *args...))>) { + if ((... && bool(args))) + std::invoke(std::forward<FnT>(fn), *args...); + } else + return (... && bool(args)) + ? Omittable(std::invoke(std::forward<FnT>(fn), *args...)) + : none; +} + +/** `std::optional` with tweaks + * + * The tweaks are: + * - streamlined assignment (operator=)/emplace()ment of values that can be + * used to implicitly construct the underlying type, including + * direct-list-initialisation, e.g.: + * \code + * struct S { int a; char b; } + * Omittable<S> o; + * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } + * \endcode + * - entirely deleted value(). The technical reason is that Xcode 10 doesn't + * have it; but besides that, value_or() or (after explicit checking) + * `operator*()`/`operator->()` are better alternatives within Quotient + * that doesn't practice throwing exceptions (as doesn't most of Qt). + * - ensure() to provide a safer lvalue accessor instead of operator* or + * operator->. Allows chained initialisation of nested Omittables: + * \code + * struct Inner { int member = 10; Omittable<int> innermost; }; + * struct Outer { int anotherMember = 10; Omittable<Inner> inner; }; + * Omittable<Outer> o; // = { 10, std::nullopt }; + * o.ensure().inner.ensure().innermost.emplace(42); + * \endcode + * - merge() - a soft version of operator= that only overwrites its first + * operand with the second one if the second one is not empty. + * - then() and then_or() to streamline read-only interrogation in a "monadic" + * interface. + */ +template <typename T> +class Omittable : public std::optional<T> { +public: + using base_type = std::optional<T>; + using value_type = std::decay_t<T>; + + using std::optional<T>::optional; + + // Overload emplace() and operator=() to allow passing braced-init-lists + // (the standard emplace() does direct-initialisation but + // not direct-list-initialisation). + using base_type::operator=; + Omittable& operator=(const value_type& v) + { + base_type::operator=(v); + return *this; + } + Omittable& operator=(value_type&& v) + { + base_type::operator=(std::move(v)); + return *this; + } + + using base_type::emplace; + T& emplace(const T& val) { return base_type::emplace(val); } + T& emplace(T&& val) { return base_type::emplace(std::move(val)); } + + // Use value_or() or check (with operator! or has_value) before accessing + // with operator-> or operator* + // The technical reason is that Xcode 10 has incomplete std::optional + // that has no value(); but using value() may also mean that you rely + // on the optional throwing an exception (which is not an assumed practice + // throughout Quotient) or that you spend unnecessary CPU cycles on + // an extraneous has_value() check. + auto& value() = delete; + const auto& value() const = delete; + + template <typename U> + value_type& ensure(U&& defaultValue = value_type {}) + { + return this->has_value() ? this->operator*() + : this->emplace(std::forward<U>(defaultValue)); + } + value_type& ensure(const value_type& defaultValue) + { + return ensure<>(defaultValue); + } + value_type& ensure(value_type&& defaultValue) + { + return ensure<>(std::move(defaultValue)); + } + + //! 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 std::optional<T1>& other) + -> std::enable_if_t<std::is_convertible_v<T1, T>, bool> + { + if (!other || (this->has_value() && **this == *other)) + return false; + this->emplace(*other); + return true; + } + + // The below is inspired by the proposed std::optional monadic operations + // (http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2021/p0798r6.html). + + //! \brief Lift a callable into the Omittable + //! + //! 'Lifting', as used in functional programming, means here invoking + //! a callable (e.g., a function) on the contents of the Omittable if it has + //! any and wrapping the returned value (that may be of a different type T2) + //! into a new Omittable\<T2>. If the current Omittable is empty, + //! the invocation is skipped altogether and Omittable\<T2>{none} is + //! returned instead. + //! \note if \p fn already returns an Omittable (i.e., it is a 'functor', + //! in functional programming terms), then() will not wrap another + //! Omittable around but will just return what \p fn returns. The + //! same doesn't hold for the parameter: if \p fn accepts an Omittable + //! you have to wrap it in another Omittable before calling then(). + //! \return `none` if the current Omittable has `none`; + //! otherwise, the Omittable returned from a call to \p fn + //! \tparam FnT a callable with \p T (or <tt>const T&</tt>) + //! returning Omittable<T2>, T2 is any supported type + //! \sa then_or, transform + template <typename FnT> + auto then(FnT&& fn) const + { + return lift(std::forward<FnT>(fn), *this); + } + + //! \brief Lift a callable into the rvalue Omittable + //! + //! This is an rvalue overload for then(). + template <typename FnT> + auto then(FnT&& fn) + { + return lift(std::forward<FnT>(fn), *this); + } + + //! \brief Lift a callable into the const lvalue Omittable, with a fallback + //! + //! This effectively does the same what then() does, except that it returns + //! a value of type returned by the callable, or the provided fallback value + //! if the current Omittable is empty. This is a typesafe version to apply + //! an operation on an Omittable without having to deal with another + //! Omittable afterwards. + template <typename FnT, typename FallbackT> + auto then_or(FnT&& fn, FallbackT&& fallback) const + { + return then(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + + //! \brief Lift a callable into the rvalue Omittable, with a fallback + //! + //! This is an overload for functions that accept rvalue + template <typename FnT, typename FallbackT> + auto then_or(FnT&& fn, FallbackT&& fallback) + { + return then(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } +}; + +template <typename T> +Omittable(T&&) -> Omittable<T>; + +//! \brief Merge the value from an optional +//! 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 std::optional<T2>& rhs) + -> std::enable_if_t<std::is_assignable_v<T1&, const T2&>, bool> +{ + if (!rhs || lhs == *rhs) + return false; + lhs = *rhs; + return true; +} + +} // namespace Quotient diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 699735d4..ef7f6f80 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -1,139 +1,103 @@ -/****************************************************************************** - * Copyright (C) 2019 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once -#include "util.h" +#include "function_traits.h" #include <QtCore/QPointer> namespace Quotient { namespace _impl { - template <typename... ArgTs> - using decorated_slot_tt = - std::function<void(QMetaObject::Connection&, const ArgTs&...)>; + enum ConnectionType { SingleShot, Until }; - template <typename SenderT, typename SignalT, typename ContextT, typename... ArgTs> - inline QMetaObject::Connection - connectDecorated(SenderT* sender, SignalT signal, ContextT* context, - decorated_slot_tt<ArgTs...> decoratedSlot, - Qt::ConnectionType connType) + template <ConnectionType CType> + inline auto connect(auto* sender, auto signal, auto* context, auto slotLike, + 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 - - // arguments are always copied (at best - COWed) to the context of - // the slot. Therefore the slot decorator receives const ArgTs&... - // rather than ArgTs&&... - // TODO (C++20): std::bind_front() instead of lambda. - c = QObject::connect(sender, signal, context, - [pc = std::move(pc), - decoratedSlot = std::move(decoratedSlot)](const ArgTs&... args) { - Q_ASSERT(*pc); // If it's been triggered, it should exist - decoratedSlot(*pc, args...); + auto pConn = std::make_unique<QMetaObject::Connection>(); + auto& c = *pConn; // Save the reference before pConn is moved from + c = QObject::connect( + sender, signal, context, + [slotLike, pConn = std::move(pConn)](const auto&... args) + // The requires-expression below is necessary to prevent Qt + // from eagerly trying to fill the lambda with more arguments + // than slotLike() (i.e., the original slot) can handle + requires requires { slotLike(args...); } { + static_assert(CType == Until || CType == SingleShot, + "Unsupported disconnection type"); + if constexpr (CType == SingleShot) { + // Disconnect early to avoid re-triggers during slotLike() + QObject::disconnect(*pConn); + // Qt kindly keeps slot objects until they do their job, + // even if they disconnect themselves in the process (see + // how doActivate() in qobject.cpp handles c->slotObj). + slotLike(args...); + } else if constexpr (CType == Until) { + if (slotLike(args...)) + QObject::disconnect(*pConn); + } }, connType); return c; } - template <typename SenderT, typename SignalT, typename ContextT, - typename... ArgTs> - inline QMetaObject::Connection - connectUntil(SenderT* sender, SignalT signal, ContextT* context, - std::function<bool(ArgTs...)> functor, - Qt::ConnectionType connType) - { - return connectDecorated(sender, signal, context, - decorated_slot_tt<ArgTs...>( - [functor = std::move(functor)](QMetaObject::Connection& c, - const ArgTs&... args) { - if (functor(args...)) - QObject::disconnect(c); - }), - connType); - } - template <typename SenderT, typename SignalT, typename ContextT, - typename... ArgTs> - inline QMetaObject::Connection - connectSingleShot(SenderT* sender, SignalT signal, ContextT* context, - std::function<void(ArgTs...)> slot, - Qt::ConnectionType connType) - { - return connectDecorated(sender, signal, context, - decorated_slot_tt<ArgTs...>( - [slot = std::move(slot)](QMetaObject::Connection& c, - const ArgTs&... args) { - QObject::disconnect(c); - slot(args...); - }), - connType); - } + + template <typename SlotT, typename ReceiverT> + concept PmfSlot = + (fn_arg_count_v<SlotT> > 0 + && std::is_base_of_v<std::decay_t<fn_arg_t<SlotT, 0>>, ReceiverT>); } // namespace _impl -/*! \brief Create a connection that self-disconnects when its "slot" returns true - * - * A slot accepted by connectUntil() is different from classic Qt slots - * in that its return value must be bool, not void. The slot's return value - * controls whether the connection should be kept; if the slot returns false, - * the connection remains; upon returning true, the slot is disconnected from - * the signal. Because of a different slot signature connectUntil() doesn't - * accept member functions as QObject::connect or Quotient::connectSingleShot - * do; you should pass a lambda or a pre-bound member function to it. - */ -template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT> -inline auto connectUntil(SenderT* sender, SignalT signal, ContextT* context, - const FunctorT& slot, +//! \brief Create a connection that self-disconnects when its slot returns true +//! +//! A slot accepted by connectUntil() is different from classic Qt slots +//! in that its return value must be bool, not void. Because of that different +//! signature connectUntil() doesn't accept member functions in the way +//! QObject::connect or Quotient::connectSingleShot do; you should pass a lambda +//! or a pre-bound member function to it. +//! \return whether the connection should be dropped; false means that the +//! connection remains; upon returning true, the slot is disconnected +//! from the signal. +inline auto connectUntil(auto* sender, auto signal, auto* context, + auto smartSlot, Qt::ConnectionType connType = Qt::AutoConnection) { - return _impl::connectUntil(sender, signal, context, wrap_in_function(slot), - connType); + return _impl::connect<_impl::Until>(sender, signal, context, smartSlot, + connType); } -/// Create a connection that self-disconnects after triggering on the signal -template <typename SenderT, typename SignalT, typename ContextT, typename FunctorT> -inline auto connectSingleShot(SenderT* sender, SignalT signal, - ContextT* context, const FunctorT& slot, +//! Create a connection that self-disconnects after triggering on the signal +template <typename ContextT, typename SlotT> +inline auto connectSingleShot(auto* sender, auto signal, ContextT* context, + SlotT slot, Qt::ConnectionType connType = Qt::AutoConnection) { - return _impl::connectSingleShot( - sender, signal, context, wrap_in_function(slot), connType); -} - -// Specialisation for usual Qt slots passed as pointers-to-members. -template <typename SenderT, typename SignalT, typename ReceiverT, - typename SlotObjectT, typename... ArgTs> -inline auto connectSingleShot(SenderT* sender, SignalT signal, - ReceiverT* receiver, - void (SlotObjectT::*slot)(ArgTs...), - Qt::ConnectionType connType = Qt::AutoConnection) -{ - // TODO: when switching to C++20, use std::bind_front() instead - return _impl::connectSingleShot(sender, signal, receiver, - wrap_in_function( - [receiver, slot](const ArgTs&... args) { - (receiver->*slot)(args...); - }), - connType); +#if QT_VERSION_MAJOR >= 6 + return QObject::connect(sender, signal, context, slot, + Qt::ConnectionType(connType + | Qt::SingleShotConnection)); +#else + // In case of classic Qt pointer-to-member-function slots the receiver + // object has to be pre-bound to the slot to make it self-contained + if constexpr (_impl::PmfSlot<SlotT, ContextT>) { + auto&& boundSlot = +# if __cpp_lib_bind_front // Needs Apple Clang 13 (other platforms are fine) + std::bind_front(slot, context); +# else + [context, slot](const auto&... args) + requires requires { (context->*slot)(args...); } + { + (context->*slot)(args...); + }; +# endif + return _impl::connect<_impl::SingleShot>( + sender, signal, context, + std::forward<decltype(boundSlot)>(boundSlot), connType); + } else { + return _impl::connect<_impl::SingleShot>(sender, signal, context, slot, + 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 bb05af05..7fec9274 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -1,20 +1,103 @@ +// SPDX-FileCopyrightText: 2019 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once +#include "quotient_export.h" + #include <qobjectdefs.h> +#include <array> + + +//! \brief Quotient replacement for the Q_FLAG/Q_DECLARE_FLAGS combination +//! +//! Although the comment in QTBUG-82295 says that Q_FLAG[_NS] "should" be +//! applied to the enum type only, Qt then doesn't allow to wrap the +//! corresponding flag type (defined with Q_DECLARE_FLAGS) into a QVariant. +//! This macro defines Q_FLAG and on top of that adds Q_ENUM_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. +//! +//! Simply put, instead of using Q_FLAG/Q_DECLARE_FLAGS combo (and struggling +//! to figure out what you should pass to Q_FLAG if you want to make it +//! wrappable in a QVariant) use the macro below, and things will just work. +//! +//! \sa https://bugreports.qt.io/browse/QTBUG-82295 +#define QUO_DECLARE_FLAGS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_IMPL(Enum) \ + Q_FLAG(Flags) + +//! \brief Quotient replacement for the Q_FLAG_NS/Q_DECLARE_FLAGS combination +//! +//! This is the equivalent of QUO_DECLARE_FLAGS for enums declared at the +//! namespace level (be sure to provide Q_NAMESPACE _in the same file_ +//! as the enum definition and this macro). +//! \sa QUO_DECLARE_FLAGS +#define QUO_DECLARE_FLAGS_NS(Flags, Enum) \ + Q_DECLARE_FLAGS(Flags, Enum) \ + Q_ENUM_NS_IMPL(Enum) \ + Q_FLAG_NS(Flags) + namespace Quotient { -Q_NAMESPACE +Q_NAMESPACE_EXPORT(QUOTIENT_API) -/** 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 }; +// 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 : uint16_t { + // 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 std::array MembershipStrings { + // The order MUST be the same as the order in the Membership 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) + +[[maybe_unused]] constexpr std::array JoinStateStrings { + 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) -enum UriResolveResult : short { +//! \brief The result of URI resolution using UriResolver +//! \sa UriResolver +enum UriResolveResult : int8_t { StillResolving = -1, UriResolved = 0, CouldNotResolve, @@ -24,6 +107,20 @@ enum UriResolveResult : short { }; Q_ENUM_NS(UriResolveResult) +enum class RoomType : uint8_t { + Space = 0, + Undefined = 0xFF, +}; +Q_ENUM_NS(RoomType) + +[[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" }; + +enum class EncryptionType : uint8_t { + MegolmV1AesSha2 = 0, + Undefined = 0xFF, +}; +Q_ENUM_NS(EncryptionType) + } // 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/quotient_export.h b/lib/quotient_export.h new file mode 100644 index 00000000..56767443 --- /dev/null +++ b/lib/quotient_export.h @@ -0,0 +1,25 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include <QtCore/qglobal.h> + +#ifdef QUOTIENT_STATIC +# define QUOTIENT_API +# define QUOTIENT_HIDDEN +#else +# ifndef QUOTIENT_API +# ifdef BUILDING_SHARED_QUOTIENT + /* We are building this library */ +# define QUOTIENT_API Q_DECL_EXPORT +# else + /* We are using this library */ +# define QUOTIENT_API Q_DECL_IMPORT +# endif +# endif + +# ifndef QUOTIENT_HIDDEN +# define QUOTIENT_HIDDEN Q_DECL_HIDDEN +# endif +#endif diff --git a/lib/room.cpp b/lib/room.cpp index 7631abe1..0cf818ce 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -1,37 +1,33 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> +// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> +// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "room.h" #include "avatar.h" #include "connection.h" #include "converters.h" -#include "e2ee.h" #include "syncdata.h" #include "user.h" +#include "eventstats.h" +#include "roomstateview.h" +#include "qt_connection_util.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" #include "csapi/inviting.h" #include "csapi/kicking.h" #include "csapi/leaving.h" -#include "csapi/receipts.h" #include "csapi/read_markers.h" +#include "csapi/receipts.h" #include "csapi/redaction.h" #include "csapi/room_send.h" #include "csapi/room_state.h" @@ -39,28 +35,24 @@ #include "csapi/rooms.h" #include "csapi/tags.h" -#include "events/callanswerevent.h" -#include "events/callcandidatesevent.h" -#include "events/callhangupevent.h" -#include "events/callinviteevent.h" +#include "events/callevents.h" #include "events/encryptionevent.h" #include "events/reactionevent.h" #include "events/receiptevent.h" #include "events/redactionevent.h" #include "events/roomavatarevent.h" +#include "events/roomcanonicalaliasevent.h" #include "events/roomcreateevent.h" #include "events/roommemberevent.h" +#include "events/roompowerlevelsevent.h" #include "events/roomtombstoneevent.h" #include "events/simplestateevents.h" #include "events/typingevent.h" -#include "events/roompowerlevelsevent.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" -#include "events/roomcanonicalaliasevent.h" #include <QtCore/QDir> #include <QtCore/QHash> -#include <QtCore/QMimeDatabase> #include <QtCore/QPointer> #include <QtCore/QRegularExpression> #include <QtCore/QStringBuilder> // for efficient string concats (operator%) @@ -71,13 +63,15 @@ #include <functional> #ifdef Quotient_E2EE_ENABLED -#include <account.h> // QtOlm -#include <errors.h> // QtOlm -#include <groupsession.h> // QtOlm +#include "e2ee/e2ee.h" +#include "e2ee/qolmaccount.h" +#include "e2ee/qolminboundsession.h" +#include "e2ee/qolmutility.h" +#include "database.h" #endif // Quotient_E2EE_ENABLED + using namespace Quotient; -using namespace QtOlm; using namespace std::placeholders; using std::move; #if !(defined __GLIBCXX__ && __GLIBCXX__ <= 20150123) @@ -109,7 +103,7 @@ public: static decltype(baseState) stubbedState; /// The state of the room at syncEdge() /// \sa syncEdge - QHash<StateEventKey, const StateEventBase*> currentState; + RoomStateView currentState; /// Servers with aliases for this room except the one of the local user /// \sa Room::remoteAliases QSet<QString> aliasServers; @@ -120,27 +114,31 @@ public: // A map from evtId to a map of relation type to a vector of event // pointers. Not using QMultiHash, because we want to quickly return // a number of relations for a given event without enumerating them. - QHash<QPair<QString, QString>, RelatedEvents> relations; + QHash<std::pair<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; + QHash<QString, ReadReceipt> lastReadReceipts; QString fullyReadUntilEventId; TagsMap tags; UnorderedMap<QString, EventPtr> accountData; QString prevBatch; QPointer<GetRoomEventsJob> eventsHistoryJob; QPointer<GetMembersByRoomJob> allMembersJob; + // Map from megolm sessionId to set of eventIds + UnorderedMap<QString, QSet<QString>> undecryptedEvents; struct FileTransferPrivateInfo { FileTransferPrivateInfo() = default; @@ -207,9 +205,9 @@ public: rev_iter_t historyEdge() const { return timeline.crend(); } Timeline::const_iterator syncEdge() const { return timeline.cend(); } - void getPreviousContent(int limit = 10); + void getPreviousContent(int limit = 10, const QString &filter = {}); - const StateEventBase* getCurrentState(const StateEventKey& evtKey) const + const StateEvent* getCurrentState(const StateEventKey& evtKey) const { const auto* evt = currentState.value(evtKey, nullptr); if (!evt) { @@ -217,10 +215,11 @@ public: // In the absence of a real event, make a stub as-if an event // with empty content has been received. Event classes should be // prepared for empty/invalid/malicious content anyway. - stubbedState.emplace(evtKey, loadStateEvent(evtKey.first, {}, - evtKey.second)); + stubbedState.emplace( + evtKey, loadEvent<StateEvent>(evtKey.first, 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); @@ -230,61 +229,20 @@ public: return evt; } - template <typename EventT> - const EventT* getCurrentState(const QString& stateKey = {}) const - { - const StateEventKey evtKey { EventT::matrixTypeId(), stateKey }; - const auto* evt = currentState.value(evtKey, nullptr); - if (!evt) { - if (stubbedState.find(evtKey) == stubbedState.end()) { - // In the absence of a real event, make a stub as-if an event - // with empty content has been received. Event classes should be - // prepared for empty/invalid/malicious content anyway. - stubbedState.emplace( - evtKey, makeEvent<EventT>(basicStateEventJson( - EventT::matrixTypeId(), {}, evtKey.second))); - qCDebug(STATE) << "A new stub event created for key {" - << evtKey.first << evtKey.second << "}"; - } - evt = stubbedState[evtKey].get(); - Q_ASSERT(evt); - } - Q_ASSERT(evt->type() == EventT::typeId() - && evt->matrixType() == EventT::matrixTypeId() - && evt->stateKey() == stateKey); - return static_cast<const EventT*>(evt); - } - -// template <typename EventT> -// const auto& getCurrentStateContent(const QString& stateKey = {}) const -// { -// if (const auto* evt = -// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr)) -// return evt->content(); -// return EventT::content_type() -// } - - bool isEventNotable(const TimelineItem& ti) const - { - return !ti->isRedacted() && ti->senderId() != connection->userId() - && is<RoomMessageEvent>(*ti) - && ti.viewAs<RoomMessageEvent>()->replacedEvent().isEmpty(); - } - template <typename EventArrayT> Changes updateStateFrom(EventArrayT&& events) { - Changes changes = NoChange; + Changes changes {}; if (!events.empty()) { QElapsedTimer et; et.start(); for (auto&& eptr : events) { const auto& evt = *eptr; Q_ASSERT(evt.isStateEvent()); - // Update baseState afterwards to make sure that the old state - // is valid and usable inside processStateEvent - changes |= q->processStateEvent(evt); - baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); + if (auto change = q->processStateEvent(evt); change) { + changes |= change; + baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); + } } if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) @@ -296,6 +254,9 @@ public: 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. @@ -312,13 +273,20 @@ public: * Remove events from the passed container that are already in the timeline */ void dropDuplicateEvents(RoomEvents& events) const; - - void setLastReadReceipt(User* u, rev_iter_t newMarker, - QString newEvtId = {}); + void decryptIncomingEvents(RoomEvents& events); + + //! \brief update last receipt record for a given user + //! + //! \return previous event id of the receipt if the new receipt changed + //! it, or `none` if no change took place + Omittable<QString> setLastReadReceipt(const QString& userId, rev_iter_t newMarker, + ReadReceipt newReceipt = {}); + Changes setLocalLastReadReceipt(const rev_iter_t& newMarker, + ReadReceipt newReceipt = {}, + bool deferStatsUpdate = false); Changes setFullyReadMarker(const QString &eventId); - Changes updateUnreadCount(const rev_iter_t& from, const rev_iter_t& to); - Changes recalculateUnreadCount(bool force = false); - void markMessagesAsRead(const rev_iter_t &upToMarker); + Changes updateStats(const rev_iter_t& from, const rev_iter_t& to); + bool markMessagesAsRead(const rev_iter_t& upToMarker); void getAllMembers(); @@ -330,12 +298,16 @@ 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); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); - SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event) + SetRoomStateWithKeyJob* requestSetState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) { // if (event.roomId().isEmpty()) // event.setRoomId(id); @@ -343,14 +315,8 @@ public: // event.setSender(connection->userId()); // TODO: Queue up state events sending (see #133). // TODO: Maybe addAsPending() as well, despite having no txnId - return connection->callApi<SetRoomStateWithKeyJob>( - id, event.matrixType(), event.stateKey(), event.contentJson()); - } - - template <typename EvT, typename... ArgTs> - auto requestSetState(ArgTs&&... args) - { - return requestSetState(EvT(std::forward<ArgTs>(args)...)); + return connection->callApi<SetRoomStateWithKeyJob>(id, evtType, stateKey, + contentJson); } /*! Apply redaction to the timeline @@ -376,87 +342,122 @@ public: bool isLocalUser(const User* u) const { return u == q->localUser(); } #ifdef Quotient_E2EE_ENABLED - // A map from <sessionId, messageIndex> to <event_id, origin_server_ts> - QHash<QPair<QString, uint32_t>, QPair<QString, QDateTime>> - groupSessionIndexRecord; // TODO: cache - // A map from senderKey to a map of sessionId to InboundGroupSession - // Not using QMultiHash, because we want to quickly return - // a number of relations for a given event without enumerating them. - QHash<QPair<QString, QString>, InboundGroupSession*> groupSessions; // TODO: - // cache - bool addInboundGroupSession(QString senderKey, QString sessionId, - QString sessionKey) + UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions; + int currentMegolmSessionMessageCount = 0; + //TODO save this to database + unsigned long long currentMegolmSessionCreationTimestamp = 0; + QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr; + + bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, + const QString& senderId, + const QString& olmSessionId) { - if (groupSessions.contains({ senderKey, sessionId })) { - qCDebug(E2EE) << "Inbound Megolm session" << sessionId - << "with senderKey" << senderKey << "already exists"; + if (groupSessions.contains(sessionId)) { + qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists"; return false; } - InboundGroupSession* megolmSession; - try { - megolmSession = new InboundGroupSession(sessionKey.toLatin1(), - InboundGroupSession::Init, - q); - } catch (OlmError* e) { - qCDebug(E2EE) << "Unable to create new InboundGroupSession" - << e->what(); - return false; - } - if (megolmSession->id() != sessionId) { - qCDebug(E2EE) << "Session ID mismatch in m.room_key event sent " - "from sender with key" - << senderKey; + auto expectedMegolmSession = QOlmInboundGroupSession::create(sessionKey); + Q_ASSERT(expectedMegolmSession.has_value()); + auto&& megolmSession = *expectedMegolmSession; + if (megolmSession->sessionId() != sessionId) { + qCWarning(E2EE) << "Session ID mismatch in m.room_key event"; return false; } - groupSessions.insert({ senderKey, sessionId }, megolmSession); + megolmSession->setSenderId(senderId); + megolmSession->setOlmSessionId(olmSessionId); + qCWarning(E2EE) << "Adding inbound session"; + connection->saveMegolmSession(q, *megolmSession); + groupSessions[sessionId] = std::move(megolmSession); return true; } QString groupSessionDecryptMessage(QByteArray cipher, - const QString& senderKey, const QString& sessionId, const QString& eventId, - QDateTime timestamp) + QDateTime timestamp, + const QString& senderId) { - std::pair<QString, uint32_t> decrypted; - QPair<QString, QString> senderSessionPairKey = - qMakePair(senderKey, sessionId); - if (!groupSessions.contains(senderSessionPairKey)) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "The sender's device has not sent us the keys for " - "this message"; - return QString(); + auto groupSessionIt = groupSessions.find(sessionId); + if (groupSessionIt == groupSessions.end()) { + // qCWarning(E2EE) << "Unable to decrypt event" << eventId + // << "The sender's device has not sent us the keys for " + // "this message"; + return {}; } - InboundGroupSession* senderSession = - groupSessions.value(senderSessionPairKey); - if (!senderSession) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "senderSessionPairKey:" << senderSessionPairKey; - return QString(); + auto& senderSession = groupSessionIt->second; + if (senderSession->senderId() != senderId) { + qCWarning(E2EE) << "Sender from event does not match sender from session"; + return {}; } - try { - decrypted = senderSession->decrypt(cipher); - } catch (OlmError* e) { - qCDebug(E2EE) << "Unable to decrypt event" << eventId - << "with matching megolm session:" << e->what(); - return QString(); + auto decryptResult = senderSession->decrypt(cipher); + if(!decryptResult) { + qCWarning(E2EE) << "Unable to decrypt event" << eventId + << "with matching megolm session:" << decryptResult.error(); + return {}; } - QPair<QString, QDateTime> properties = groupSessionIndexRecord.value( - qMakePair(senderSession->id(), decrypted.second)); - if (properties.first.isEmpty()) { - groupSessionIndexRecord.insert(qMakePair(senderSession->id(), - decrypted.second), - qMakePair(eventId, timestamp)); + const auto& [content, index] = *decryptResult; + const auto& [recordEventId, ts] = + q->connection()->database()->groupSessionIndexRecord( + q->id(), senderSession->sessionId(), index); + if (recordEventId.isEmpty()) { + q->connection()->database()->addGroupSessionIndexRecord( + q->id(), senderSession->sessionId(), index, eventId, + timestamp.toMSecsSinceEpoch()); } else { - if ((properties.first != eventId) - || (properties.second != timestamp)) { - qCDebug(E2EE) << "Detected a replay attack on event" << eventId; - return QString(); + if ((eventId != recordEventId) + || (ts != timestamp.toMSecsSinceEpoch())) { + qCWarning(E2EE) << "Detected a replay attack on event" << eventId; + return {}; } } + return content; + } + + bool shouldRotateMegolmSession() const + { + const auto* encryptionConfig = currentState.get<EncryptionEvent>(); + if (!encryptionConfig || !encryptionConfig->useEncryption()) + return false; + + const auto rotationInterval = encryptionConfig->rotationPeriodMs(); + const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs(); + return currentOutboundMegolmSession->messageCount() + >= rotationMessageCount + || currentOutboundMegolmSession->creationTime().addMSecs( + rotationInterval) + < QDateTime::currentDateTime(); + } + + bool hasValidMegolmSession() const + { + if (!q->usesEncryption()) { + return false; + } + return currentOutboundMegolmSession != nullptr; + } + + void createMegolmSession() { + qCDebug(E2EE) << "Creating new outbound megolm session for room " + << q->objectName(); + currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); - return decrypted.first; + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), + currentOutboundMegolmSession->sessionKey(), + q->localUser()->id(), "SELF"_ls); + } + + QMultiHash<QString, QString> getDevicesWithoutKey() const + { + QMultiHash<QString, QString> devices; + for (const auto& user : q->users()) + for (const auto& deviceId : connection->devicesForUser(user->id())) + devices.insert(user->id(), deviceId); + + return connection->database()->devicesWithoutKey( + id, devices, currentOutboundMegolmSession->sessionId()); } #endif // Quotient_E2EE_ENABLED @@ -477,12 +478,37 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) // https://marcmutz.wordpress.com/translated-articles/pimp-my-pimpl-%E2%80%94-reloaded/ d->q = this; d->displayname = d->calculateDisplayname(); // Set initial "Empty room" name - connectUntil(connection, &Connection::loadedRoomState, this, [this](Room* r) { - if (this == r) - emit baseStateLoaded(); - return this == r; // loadedRoomState fires only once per room +#ifdef Quotient_E2EE_ENABLED + connectSingleShot(this, &Room::encryption, this, [this, connection](){ + connection->encryptionUpdate(this); + }); + connect(this, &Room::userAdded, this, [this, connection](){ + if(usesEncryption()) { + connection->encryptionUpdate(this); + } + }); + d->groupSessions = connection->loadRoomMegolmSessions(this); + d->currentOutboundMegolmSession = + connection->loadCurrentOutboundMegolmSession(this->id()); + if (d->shouldRotateMegolmSession()) { + d->currentOutboundMegolmSession = nullptr; + } + connect(this, &Room::userRemoved, this, [this](){ + if (!usesEncryption()) { + return; + } + if (d->hasValidMegolmSession()) { + d->createMegolmSession(); + } + qCDebug(E2EE) << "Invalidating current megolm session because user left"; + }); - qCDebug(STATE) << "New" << toCString(initialJoinState) << "Room:" << id; + + connect(this, &Room::beforeDestruction, this, [=](){ + connection->database()->clearRoomData(id); + }); +#endif + qCDebug(STATE) << "New" << terse << initialJoinState << "Room:" << id; } Room::~Room() { delete d; } @@ -491,8 +517,8 @@ const QString& Room::id() const { return d->id; } QString Room::version() const { - const auto v = d->getCurrentState<RoomCreateEvent>()->version(); - return v.isEmpty() ? QStringLiteral("1") : v; + const auto v = currentState().query(&RoomCreateEvent::version); + return v && !v->isEmpty() ? *v : QStringLiteral("1"); } bool Room::isUnstable() const @@ -503,7 +529,10 @@ bool Room::isUnstable() const QString Room::predecessorId() const { - return d->getCurrentState<RoomCreateEvent>()->predecessor().roomId; + if (const auto* evt = currentState().get<RoomCreateEvent>()) + return evt->predecessor().roomId; + + return {}; } Room* Room::predecessor(JoinStates statesFilter) const @@ -518,7 +547,8 @@ Room* Room::predecessor(JoinStates statesFilter) const QString Room::successorId() const { - return d->getCurrentState<RoomTombstoneEvent>()->successorRoomId(); + return currentState().queryOr(&RoomTombstoneEvent::successorRoomId, + QString()); } Room* Room::successor(JoinStates statesFilter) const @@ -545,50 +575,56 @@ bool Room::allHistoryLoaded() const QString Room::name() const { - return d->getCurrentState<RoomNameEvent>()->name(); + return currentState().content<RoomNameEvent>().value; } QStringList Room::aliases() const { - const auto* evt = d->getCurrentState<RoomCanonicalAliasEvent>(); - auto result = evt->altAliases(); - if (!evt->alias().isEmpty()) - result << evt->alias(); - return result; + if (const auto* evt = currentState().get<RoomCanonicalAliasEvent>()) { + auto result = evt->altAliases(); + if (!evt->alias().isEmpty()) + result << evt->alias(); + return result; + } + return {}; } QStringList Room::altAliases() const { - return d->getCurrentState<RoomCanonicalAliasEvent>()->altAliases(); + return currentState().content<RoomCanonicalAliasEvent>().altAliases; } -QStringList Room::localAliases() const +QString Room::canonicalAlias() const { - return d->getCurrentState<RoomAliasesEvent>( - connection()->domain()) - ->aliases(); + return currentState().queryOr(&RoomCanonicalAliasEvent::alias, QString()); } -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::displayName() const { return d->displayname; } + +QStringList Room::pinnedEventIds() const { + return currentState().queryOr(&RoomPinnedEvent::pinnedEvents, QStringList()); } -QString Room::canonicalAlias() const +QVector<const Quotient::RoomEvent*> Quotient::Room::pinnedEvents() const { - return d->getCurrentState<RoomCanonicalAliasEvent>()->alias(); + QVector<const RoomEvent*> pinnedEvents; + for (const auto& evtId : pinnedEventIds()) + if (const auto& it = findInTimeline(evtId); it != historyEdge()) + pinnedEvents.append(it->event()); + + return pinnedEvents; } -QString Room::displayName() const { return d->displayname; } +QString Room::displayNameForHtml() const +{ + return displayName().toHtmlEscaped(); +} void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const { - return d->getCurrentState<RoomTopicEvent>()->topic(); + return currentState().queryOr(&RoomTopicEvent::topic, QString()); } QString Room::avatarMediaId() const { return d->avatar.mediaId(); } @@ -603,13 +639,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 {}; } @@ -621,9 +657,19 @@ User* Room::user(const QString& userId) const JoinState Room::memberJoinState(User* user) const { - return user != nullptr && d->membersMap.contains(user->name(this), user) - ? JoinState::Join - : JoinState::Leave; + return d->membersMap.contains(user->name(this), user) ? JoinState::Join + : JoinState::Leave; +} + +Membership Room::memberState(const QString& userId) const +{ + return currentState().queryOr(userId, &RoomMemberEvent::membership, + Membership::Leave); +} + +bool Room::isMember(const QString& userId) const +{ + return memberState(userId) == Membership::Join; } JoinState Room::joinState() const { return d->joinState; } @@ -634,195 +680,277 @@ 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); } -void Room::Private::setLastReadReceipt(User* u, rev_iter_t newMarker, - QString newEvtId) +Omittable<QString> Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, + ReadReceipt newReceipt) { - if (!u) { - Q_ASSERT(u != nullptr); // For Debug builds - qCCritical(MAIN) << "Empty user, skipping read receipt registration"; - return; // For Release builds - } - if (q->memberJoinState(u) != JoinState::Join) { - qCWarning(EPHEMERAL) - << "Won't record read receipt for non-member" << u->id(); - return; - } - - if (newMarker == historyEdge() && !newEvtId.isEmpty()) - newMarker = q->findInTimeline(newEvtId); + if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty()) + newMarker = q->findInTimeline(newReceipt.eventId); if (newMarker != historyEdge()) { - // NB: with reverse iterators, timeline history >= sync edge - if (newMarker >= q->readMarker(u)) { - qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() - << "is at or behind the old one, skipping"; - return; - } - // 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() != u->id(); - }) - - 1; - newEvtId = (*eagerMarker)->id(); - if (eagerMarker != newMarker.base() - 1) // &*(rIt.base() - 1) === &*rIt - qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << u->id() - << "to" << newEvtId; - } + return ti->senderId() != userId; + }); + // eagerMarker is now just after the desired event for newMarker + if (eagerMarker != newMarker.base()) { + newMarker = rev_iter_t(eagerMarker); + qDebug(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. + // This logic tackles, in particular, the case when the new 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; in that case, + // the previous marker is kept because read receipts are not supposed + // to move backwards. If neither new nor old event is found, the new receipt + // is blindly stored, in a hope it's also "newer" in the timeline. + // NB: with reverse iterators, timeline history edge >= sync edge + if (prevEventId == newReceipt.eventId + || newMarker > q->findInTimeline(prevEventId)) + return {}; - auto& storedId = lastReadEventIds[u]; - if (storedId == newEvtId) - return; // Finally make the change - eventIdReadUsers.remove(storedId, u); - eventIdReadUsers.insert(newEvtId, u); - swap(storedId, newEvtId); // Now newEvtId actually stores the old eventId - qCDebug(EPHEMERAL) << "The new read receipt for" << u->id() << "is at" - << storedId; - emit q->lastReadEventChanged(u); - if (!isLocalUser(u)) - emit q->readMarkerForUserMoved(u, newEvtId, storedId); + + 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); // NB: qCDebug can't be used like that + dbg << "The new read receipt for" << userId << "is now at"; + if (newMarker == historyEdge()) + dbg << storedReceipt.eventId; + else + dbg << *newMarker; + } + + // NB: This method, unlike setLocalLastReadReceipt, doesn't emit + // lastReadEventChanged() to avoid numerous emissions when many read + // receipts arrive. It can be called thousands of times during an initial + // sync, e.g. + // TODO: remove in 0.8 + if (const auto member = q->user(userId); !isLocalUser(member)) + QT_IGNORE_DEPRECATIONS(emit q->readMarkerForUserMoved( + member, prevEventId, storedReceipt.eventId);) + return prevEventId; +} + +Room::Changes Room::Private::setLocalLastReadReceipt(const rev_iter_t& newMarker, + ReadReceipt newReceipt, + bool deferStatsUpdate) +{ + auto prevEventId = + setLastReadReceipt(connection->userId(), newMarker, move(newReceipt)); + if (!prevEventId) + return Change::None; + Changes changes = Change::Other; + if (!deferStatsUpdate) { + if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(*prevEventId), + newMarker)) { + qDebug(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 + } + emit q->lastReadEventChanged({ connection->userId() }); + return changes; } -Room::Changes 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()); - auto fullyReadMarker = q->readMarker(); + 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 + && setLocalLastReadReceipt(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 NoChange; // What's arrived is already fully read + 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 special case when the last fully read event id refers to an - // event that has just arrived. In this case we should recalculate - // unreadMessages to get an exact number instead of an estimation - // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). - // For the same reason (switching from the estimation to the exact - // number) this branch always emits unreadMessagesChanged() and returns - // UnreadNotifsChange, even if the estimation luckily matched the exact - // result. - if (fullyReadMarker < to) - return recalculateUnreadCount(true); - - // At this point the fully read marker is somewhere beyond the "oldest" - // message from the arrived batch - add up newly arrived messages to - // the current counter, instead of a complete recalculation. - Q_ASSERT(to <= fullyReadMarker); + // 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; + } + } - 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 in" - << q->objectName() << "took" << et; - - if (newUnreadMessages == 0) - return NoChange; - - // 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() == historyEdge() - ? "in total at least" - : "in total") - << unreadMessages << "unread message(s)"; - emit q->unreadMessagesChanged(q); - return UnreadNotifsChange; -} - -Room::Changes Room::Private::recalculateUnreadCount(bool force) -{ - // The recalculation logic assumes that the fully read marker points at - // a specific position in the timeline - Q_ASSERT(q->readMarker() != historyEdge()); - const auto oldUnreadCount = unreadMessages; - QElapsedTimer et; - et.start(); - unreadMessages = - int(count_if(timeline.crbegin(), q->readMarker(), - [this](const auto& ti) { return isEventNotable(ti); })); - if (et.nsecsElapsed() > profilerMinNsecs() / 10) - qCDebug(PROFILER) << "Recounting unread messages in" << q->objectName() - << "took" << et; + // 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); - // See https://github.com/quotient-im/libQuotient/wiki/unread_count - if (unreadMessages == 0) - unreadMessages = -1; + const auto newStats = EventStats::fromRange(q, from, to); + Q_ASSERT(!newStats.isEstimate); + if (newStats.empty()) + return changes; - if (!force && unreadMessages == oldUnreadCount) - return NoChange; + 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 (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); - return 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::setFullyReadMarker(const QString& eventId) { if (fullyReadUntilEventId == eventId) - return NoChange; + return Change::None; + + 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; - emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId); - - Changes changes = ReadMarkerChange; - if (const auto rm = q->readMarker(); rm != historyEdge()) { - // Pull read receipt if it's behind - if (auto rr = q->readMarker(q->localUser()); rr > rm) - setLastReadReceipt(q->localUser(), rm); - changes |= recalculateUnreadCount(); + 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 |= setLocalLastReadReceipt(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 + QT_IGNORE_DEPRECATIONS( + emit q->readMarkerMoved(prevFullyReadId, fullyReadUntilEventId);) return changes; } -void Room::Private::markMessagesAsRead(const rev_iter_t &upToMarker) +void Room::setReadReceipt(const QString& atEventId) { - if (upToMarker < q->readMarker()) { - setFullyReadMarker((*upToMarker)->id()); - // Assuming that if a read receipt was sent on a newer event, it will - // stay there instead of "un-reading" notifications/mentions from - // m.fully_read to m.read + if (const auto changes = + d->setLocalLastReadReceipt(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(QString uptoEventId) +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 @@ -830,8 +958,9 @@ bool Room::canSwitchVersions() const if (!successorId().isEmpty()) return false; // No one can upgrade a room that's already upgraded - if (const auto* plEvt = d->getCurrentState<RoomPowerLevelsEvent>()) { - const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id()); + if (const auto* plEvt = currentState().get<RoomPowerLevelsEvent>()) { + const auto currentUserLevel = + plEvt->powerLevelForUser(localUser()->id()); const auto tombstonePowerLevel = plEvt->powerLevelForState("m.room.tombstone"_ls); return currentUserLevel >= tombstonePowerLevel; @@ -839,16 +968,45 @@ 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()); +} + +Notification Room::checkForNotifications(const TimelineItem &ti) +{ + return { Notification::None }; +} + +bool Room::hasUnreadMessages() const { return !d->partiallyReadStats.empty(); } + +int countFromStats(const EventStats& s) +{ + return s.empty() ? -1 : int(s.notableCount); +} + +int Room::unreadCount() const { return countFromStats(partiallyReadStats()); } + +EventStats Room::partiallyReadStats() const { return d->partiallyReadStats; } -int Room::unreadCount() const { return d->unreadMessages; } +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(); } -Room::rev_iter_t Room::timelineEdge() const { return d->historyEdge(); } - TimelineItem::index_t Room::minTimelineIndex() const { return d->timeline.empty() ? 0 : d->timeline.front().index(); @@ -867,7 +1025,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); } @@ -898,28 +1056,38 @@ Room::findPendingEvent(const QString& txnId) const }); } -const Room::RelatedEvents Room::relatedEvents(const QString& evtId, - const char* relType) const +const Room::RelatedEvents Room::relatedEvents( + const QString& evtId, EventRelation::reltypeid_t relType) const { return d->relations.value({ evtId, relType }); } -const Room::RelatedEvents Room::relatedEvents(const RoomEvent& evt, - const char* relType) const +const Room::RelatedEvents Room::relatedEvents( + const RoomEvent& evt, EventRelation::reltypeid_t relType) const { return relatedEvents(evt.id(), relType); } +const RoomCreateEvent* Room::creation() const +{ + return currentState().get<RoomCreateEvent>(); +} + +const RoomTombstoneEvent *Room::tombstone() const +{ + return currentState().get<RoomTombstoneEvent>(); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. - if (q->joinedCount() <= membersMap.size() || isJobRunning(allMembersJob)) + if (q->joinedCount() <= membersMap.size() || isJobPending(allMembersJob)) return; 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 @@ -929,8 +1097,7 @@ void Room::Private::getAllMembers() it != syncEdge(); ++it) if (is<RoomMemberEvent>(**it)) roomChanges |= q->processStateEvent(**it); - if (roomChanges & MembersChange) - emit q->memberListChanged(); + postprocessChanges(roomChanges); emit q->allMembersLoaded(); }); } @@ -995,12 +1162,6 @@ void Room::setLastDisplayedEventId(const QString& eventId) d->lastDisplayedEventId = eventId; emit lastDisplayedEventChanged(); - if (d->displayed && marker < readMarker(localUser())) { - d->setLastReadReceipt(localUser(), marker); - connection()->callApi<PostReceiptJob>(BackgroundRequest, id(), - QStringLiteral("m.read"), - QUrl::toPercentEncoding(eventId)); - } } void Room::setLastDisplayedEvent(TimelineItem::index_t index) @@ -1012,41 +1173,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 fullyReadMarker(); } + +QString Room::readMarkerEventId() const { return lastFullyReadEventId(); } + +ReadReceipt Room::lastReadReceipt(const QString& userId) const +{ + return d->lastReadReceipts.value(userId); +} + +ReadReceipt Room::lastLocalReadReceipt() const +{ + return d->lastReadReceipts.value(localUser()->id()); +} + +Room::rev_iter_t Room::localReadReceiptMarker() const +{ + return findInTimeline(lastLocalReadReceipt().eventId); } -Room::rev_iter_t Room::readMarker() const +QString Room::lastFullyReadEventId() const { return d->fullyReadUntilEventId; } + +Room::rev_iter_t Room::fullyReadMarker() const { return findInTimeline(d->fullyReadUntilEventId); } -QString Room::readMarkerEventId() const +QSet<QString> Room::userIdsAtEvent(const QString& eventId) { - return d->fullyReadUntilEventId; + return d->eventIdReadUsers.value(eventId); } -QList<User*> Room::usersAtEventId(const QString& eventId) +QSet<User*> Room::usersAtEventId(const QString& eventId) { - return d->eventIdReadUsers.values(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; } -int Room::notificationCount() const { return d->notificationCount; } +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(); } @@ -1141,8 +1331,8 @@ void Room::setTags(TagsMap newTags, ActionScope applyOn) d->setTags(move(newTags)); connection()->callApi<SetAccountDataPerRoomJob>( - localUser()->id(), id(), TagEvent::matrixTypeId(), - TagEvent(d->tags).contentJson()); + localUser()->id(), id(), TagEvent::TypeId, + Quotient::toJson(TagEvent::content_type { d->tags })); if (propagate) { for (auto* r = this; (r = r->successor(joinStates));) @@ -1184,6 +1374,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("[/\\<>|\"*?:]"), "_"); @@ -1237,9 +1438,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 {}; @@ -1250,8 +1450,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 {}; } @@ -1316,29 +1515,49 @@ QList<User*> Room::users() const { return d->membersMap.values(); } QStringList Room::memberNames() const { + return safeMemberNames(); +} + +QStringList Room::safeMemberNames() const +{ QStringList res; res.reserve(d->membersMap.size()); - for (auto u : qAsConst(d->membersMap)) - res.append(roomMembername(u)); + for (const auto* u: std::as_const(d->membersMap)) + res.append(safeMemberName(u->id())); return res; } -int Room::memberCount() const { return d->membersMap.size(); } +QStringList Room::htmlSafeMemberNames() const +{ + QStringList res; + res.reserve(d->membersMap.size()); + for (const auto* u: std::as_const(d->membersMap)) + res.append(htmlSafeMemberName(u->id())); + + return res; +} int Room::timelineSize() const { return int(d->timeline.size()); } bool Room::usesEncryption() const { - return !d->getCurrentState<EncryptionEvent>()->algorithm().isEmpty(); + return !currentState() + .queryOr(&EncryptionEvent::algorithm, QString()) + .isEmpty(); } -const StateEventBase* Room::getCurrentState(const QString& evtType, - const QString& stateKey) const +const StateEvent* Room::getCurrentState(const QString& evtType, + const QString& stateKey) const { return d->getCurrentState({ evtType, stateKey }); } +RoomStateView Room::currentState() const +{ + return d->currentState; +} + RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) { #ifndef Quotient_E2EE_ENABLED @@ -1346,39 +1565,64 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; return {}; #else // Quotient_E2EE_ENABLED - if (encryptedEvent.algorithm() == MegolmV1AesSha2AlgoKey) { - QString decrypted = d->groupSessionDecryptMessage( - encryptedEvent.ciphertext(), encryptedEvent.senderKey(), - encryptedEvent.sessionId(), encryptedEvent.id(), - encryptedEvent.originTimestamp()); - if (decrypted.isEmpty()) { - return {}; - } - return makeEvent<RoomMessageEvent>( - QJsonDocument::fromJson(decrypted.toUtf8()).object()); + if (encryptedEvent.algorithm() != MegolmV1AesSha2AlgoKey) { + qWarning(E2EE) << "Algorithm of the encrypted event with id" + << encryptedEvent.id() << "is not decryptable by the current device"; + return {}; + } + QString decrypted = d->groupSessionDecryptMessage( + encryptedEvent.ciphertext(), encryptedEvent.sessionId(), + encryptedEvent.id(), encryptedEvent.originTimestamp(), + encryptedEvent.senderId()); + if (decrypted.isEmpty()) { + // qCWarning(E2EE) << "Encrypted message is empty"; + return {}; } - qCDebug(E2EE) << "Algorithm of the encrypted event with id" - << encryptedEvent.id() << "is not for the current device"; + auto decryptedEvent = encryptedEvent.createDecrypted(decrypted); + if (decryptedEvent->roomId() == id()) { + return decryptedEvent; + } + qCWarning(E2EE) << "Decrypted event" << encryptedEvent.id() << "not for this room; discarding."; return {}; #endif // Quotient_E2EE_ENABLED } void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, - const QString& senderKey) + const QString& senderId, + const QString& olmSessionId) { #ifndef Quotient_E2EE_ENABLED Q_UNUSED(roomKeyEvent) - Q_UNUSED(senderKey) + Q_UNUSED(senderId) + Q_UNUSED(olmSessionId) qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED if (roomKeyEvent.algorithm() != MegolmV1AesSha2AlgoKey) { qCWarning(E2EE) << "Ignoring unsupported algorithm" << roomKeyEvent.algorithm() << "in m.room_key event"; } - if (d->addInboundGroupSession(senderKey, roomKeyEvent.sessionId(), - roomKeyEvent.sessionKey())) { - qCDebug(E2EE) << "added new inboundGroupSession:" - << d->groupSessions.count(); + if (d->addInboundGroupSession(roomKeyEvent.sessionId(), + roomKeyEvent.sessionKey(), senderId, + olmSessionId)) { + qCWarning(E2EE) << "added new inboundGroupSession:" + << d->groupSessions.size(); + auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()]; + for (const auto& eventId : undecryptedEvents) { + const auto pIdx = d->eventsIndex.constFind(eventId); + if (pIdx == d->eventsIndex.cend()) + continue; + auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())]; + if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) { + if (auto decrypted = decryptMessage(*encryptedEvent)) { + // The reference will survive the pointer being moved + auto& decryptedEvent = *decrypted; + auto oldEvent = ti.replaceEvent(std::move(decrypted)); + decryptedEvent.setOriginalEvent(std::move(oldEvent)); + emit replacedEvent(ti.event(), decryptedEvent.originalEvent()); + d->undecryptedEvents[roomKeyEvent.sessionId()] -= eventId; + } + } + } } #endif // Quotient_E2EE_ENABLED } @@ -1402,29 +1646,35 @@ 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) { - const auto userName = - getCurrentState<RoomMemberEvent>(u->id())->displayName(); - // If there is exactly one namesake of the added user, signal member - // renaming for that other one because the two should be disambiguated now. + const auto maybeUserName = + currentState.query(u->id(), &RoomMemberEvent::newDisplayName); + if (!maybeUserName) + qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id() + << "has no name (even empty)"; + const auto userName = maybeUserName.value_or(QString()); const auto namesakes = membersMap.values(userName); + qCDebug(MEMBERS) << "insertMemberIntoMap(), user" << u->id() + << "with name" << userName << '-' + << namesakes.size() << "namesake(s) found"; - // Callers should check they are not adding an existing user once more. + // Callers should make sure they are not adding an existing user once more Q_ASSERT(!namesakes.contains(u)); if (namesakes.contains(u)) { // Release version whines but continues - qCCritical(STATE) << "Trying to add a user" << u->id() << "to room" - << q->objectName() << "but that's already in it"; + qCCritical(MEMBERS) << "Trying to add a user" << u->id() << "to room" + << q->objectName() << "but that's already in it"; return; } + // If there is exactly one namesake of the added user, signal member + // renaming for that other one because the two should be disambiguated now if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); @@ -1435,26 +1685,50 @@ void Room::Private::insertMemberIntoMap(User* u) void Room::Private::removeMemberFromMap(User* u) { - const auto userName = - getCurrentState<RoomMemberEvent>(u->id())->displayName(); + const auto userName = currentState.queryOr(u->id(), + &RoomMemberEvent::newDisplayName, + QString()); + qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName + << "for user" << u->id(); User* namesake = nullptr; auto namesakes = membersMap.values(userName); + // If there was one namesake besides the removed user, signal member + // renaming for it because it doesn't need to be disambiguated any more. if (namesakes.size() == 2) { - namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); + namesake = + namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); emit q->memberAboutToRename(namesake, userName); } - membersMap.remove(userName, u); - // If there was one namesake besides the removed user, signal member - // renaming for it because it doesn't need to be disambiguated any more. + if (membersMap.remove(userName, u) == 0) { + qCDebug(MEMBERS) << "No entries removed; checking the whole list"; + // Unless at the stage of initial filling, this no removed entries + // is suspicious; double-check that this user is not found in + // the whole map, and stop (for debug builds) or shout in the logs + // (for release builds) if there's one. That search is O(n), which + // may come rather expensive for larger rooms. + QElapsedTimer et; + auto it = std::find(membersMap.cbegin(), membersMap.cend(), u); + if (et.nsecsElapsed() > profilerMinNsecs() / 10) + qCDebug(MEMBERS) << "...done in" << et; + if (it != membersMap.cend()) { + // The assert (still) does more harm than good, it seems +// Q_ASSERT_X(false, __FUNCTION__, +// "Mismatched name in the room members list"); + qCCritical(MEMBERS) << "Mismatched name in the room members list;" + " avoiding the list corruption"; + membersMap.remove(it.key(), u); + } + } if (namesake) emit q->memberRenamed(namesake); } 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 @@ -1480,11 +1754,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; @@ -1492,103 +1767,209 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, return Timeline::size_type(insertedSize); } +QString Room::memberName(const QString& mxId) const +{ + // See https://github.com/matrix-org/matrix-doc/issues/1375 + if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) { + if (rme->newDisplayName()) + return *rme->newDisplayName(); + if (rme->prevContent() && rme->prevContent()->displayName) + return *rme->prevContent()->displayName; + } + return {}; +} + QString Room::roomMembername(const User* u) const { + Q_ASSERT(u != nullptr); + return disambiguatedMemberName(u->id()); +} + +QString Room::roomMembername(const QString& userId) const +{ + return disambiguatedMemberName(userId); +} + +inline QString makeFullUserName(const QString& displayName, const QString& mxId) +{ + return displayName % " (" % mxId % ')'; +} + +QString Room::disambiguatedMemberName(const QString& mxId) const +{ // See the CS spec, section 11.2.2.3 - const auto username = u->name(this); + const auto username = memberName(mxId); if (username.isEmpty()) - return u->id(); + return mxId; auto namesakesIt = qAsConst(d->membersMap).find(username); // We expect a user to be a member of the room - but technically it is - // possible to invoke roomMemberName() even for non-members. In such case + // possible to invoke this function even for non-members. In such case // we return the full name, just in case. if (namesakesIt == d->membersMap.cend()) - return u->fullName(this); + return makeFullUserName(username, mxId); auto nextUserIt = namesakesIt; if (++nextUserIt == d->membersMap.cend() || nextUserIt.key() != username) return username; // No disambiguation necessary - return u->fullName(this); // Disambiguate fully + return makeFullUserName(username, mxId); // Disambiguate fully } -QString Room::roomMembername(const QString& userId) const +QString Room::safeMemberName(const QString& userId) const { - if (auto* const u = user(userId)) - return roomMembername(u); - return {}; + return sanitized(disambiguatedMemberName(userId)); } -QString Room::safeMemberName(const QString& userId) const +QString Room::htmlSafeMemberName(const QString& userId) const { - return sanitized(roomMembername(userId)); + return safeMemberName(userId).toHtmlEscaped(); +} + +QUrl Room::memberAvatarUrl(const QString &mxId) const +{ + // See https://github.com/matrix-org/matrix-doc/issues/1375 + if (const auto rme = currentState().get<RoomMemberEvent>(mxId)) { + if (rme->newAvatarUrl()) + return *rme->newAvatarUrl(); + if (rme->prevContent() && rme->prevContent()->avatarUrl) + return *rme->prevContent()->avatarUrl; + } + return {}; +} + +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) { + qCDebug(MAIN) << "--- Updating room" << id() << "/" << objectName(); + bool firstUpdate = d->baseState.empty(); + if (d->prevBatch.isEmpty()) d->prevBatch = data.timelinePrevBatch; setJoinState(data.joinState); - Changes roomChanges = Change::NoChange; + 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); - // The order of calculation is important - don't merge these lines! - roomChanges |= d->addNewMessageEvents(move(data.timeline)); + roomChanges |= d->updateStatsFromSyncData(data, fromCache); - 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); + if (firstUpdate) + emit baseStateLoaded(); + qCDebug(MAIN) << "--- Finished updating room" << id() << "/" << objectName(); +} - 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 - // -2 is a special value to which SyncRoomData::SyncRoomData sets - // unreadCount when it's missing in the payload (to distinguish from - // explicit 0 in the payload). - if (data.unreadCount != -2 && data.unreadCount != d->unreadMessages) { - qCDebug(MESSAGES) << "Setting unread_count to" << data.unreadCount; - d->unreadMessages = data.unreadCount; - emit unreadMessagesChanged(this); - } - - // Similar to unreadCount, SyncRoomData constructor assigns -1 to - // highlightCount/notificationCount when those are missing in the payload - if (data.highlightCount != -1 && data.highlightCount != d->highlightCount) { - qCDebug(MESSAGES).nospace() - << "Highlights in " << objectName() // - << ": " << d->highlightCount << " -> " << data.highlightCount; - d->highlightCount = data.highlightCount; - emit highlightCountChanged(); - } - if (data.notificationCount != -1 - && data.notificationCount != d->notificationCount) // - { - qCDebug(MESSAGES).nospace() - << "Notifications in " << objectName() // - << ": " << d->notificationCount << " -> " << data.notificationCount; - d->notificationCount = data.notificationCount; - emit notificationCountChanged(); - } - if (roomChanges != Change::NoChange) { - d->updateDisplayname(); - emit changed(roomChanges); - if (!fromCache) - connection()->saveRoomState(this); + if (changes + & (Change::Name | Change::Aliases | Change::Members | Change::Summary)) + updateDisplayname(); + + if (changes & Change::PartiallyReadStats) { + QT_IGNORE_DEPRECATIONS( + 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" << Qt::hex << uint(changes) + << "in" << q->objectName(); + emit q->changed(changes); + if (saveState) + connection->saveRoomState(q); } RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) @@ -1608,41 +1989,73 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { - if (q->usesEncryption()) { - qCCritical(MAIN) << "Room" << q->objectName() - << "enforces encryption; sending encrypted messages " - "is not supported yet"; + if (!q->successorId().isEmpty()) { + qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; + return {}; } - if (q->successorId().isEmpty()) - return doSendEvent(addAsPending(std::move(event))); - qCWarning(MAIN) << q << "has been upgraded, event won't be sent"; - return {}; + return doSendEvent(addAsPending(std::move(event))); } QString Room::Private::doSendEvent(const RoomEvent* pEvent) { const auto txnId = pEvent->transactionId(); // TODO, #133: Enqueue the job rather than immediately trigger it. + const RoomEvent* _event = pEvent; + std::unique_ptr<EncryptedEvent> encryptedEvent; + + if (q->usesEncryption()) { +#ifndef Quotient_E2EE_ENABLED + qWarning() << "This build of libQuotient does not support E2EE."; + return {}; +#else + if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { + createMegolmSession(); + } + // Send the session to other people + connection->sendSessionKeyToDevices( + id, currentOutboundMegolmSession->sessionId(), + currentOutboundMegolmSession->sessionKey(), getDevicesWithoutKey(), + currentOutboundMegolmSession->sessionMessageIndex()); + + const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); + currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); + encryptedEvent = makeEvent<EncryptedEvent>( + encrypted, q->connection()->olmAccount()->identityKeys().curve25519, + q->connection()->deviceId(), + currentOutboundMegolmSession->sessionId()); + encryptedEvent->setTransactionId(connection->generateTxnId()); + encryptedEvent->setRoomId(id); + encryptedEvent->setSender(connection->userId()); + if(pEvent->contentJson().contains("m.relates_to"_ls)) { + encryptedEvent->setRelation(pEvent->contentJson()["m.relates_to"_ls].toObject()); + } + // We show the unencrypted event locally while pending. The echo check will throw the encrypted version out + _event = encryptedEvent.get(); +#endif + } + if (auto call = connection->callApi<SendMessageJob>(BackgroundRequest, id, - pEvent->matrixType(), txnId, - pEvent->contentJson())) { + _event->matrixType(), txnId, + _event->contentJson())) { Room::connect(call, &BaseJob::sentRequest, q, [this, txnId] { auto it = q->findPendingEvent(txnId); if (it == unsyncedEvents.end()) { - qCWarning(EVENTS) << "Pending event for transaction" << txnId + qWarning(EVENTS) << "Pending event for transaction" << txnId << "not found - got synced so soon?"; return; } it->setDeparted(); - qCDebug(EVENTS) << "Event txn" << txnId << "has departed"; emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); }); - Room::connect(call, &BaseJob::failure, q, - std::bind(&Room::Private::onEventSendingFailure, this, - txnId, call)); - Room::connect(call, &BaseJob::success, q, [this, call, txnId] { + Room::connect(call, &BaseJob::result, q, [this, txnId, call] { + if (!call->status().good()) { + onEventSendingFailure(txnId, call); + return; + } auto it = q->findPendingEvent(txnId); if (it != unsyncedEvents.end()) { if (it->deliveryStatus() != EventStatus::ReachedServer) { @@ -1650,7 +2063,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) emit q->pendingEventChanged(int(it - unsyncedEvents.begin())); } } else - qCDebug(EVENTS) << "Pending event for transaction" << txnId + qDebug(EVENTS) << "Pending event for transaction" << txnId << "already merged"; emit q->messageSent(txnId, call->eventId()); @@ -1686,7 +2099,7 @@ QString Room::retryMessage(const QString& txnId) << "File for transaction" << txnId << "has already been uploaded, bypassing re-upload"; } else { - if (isJobRunning(transferIt->job)) { + if (isJobPending(transferIt->job)) { qCDebug(MESSAGES) << "Abandoning the upload job for transaction" << txnId << "and starting again"; transferIt->job->abandon(); @@ -1708,6 +2121,10 @@ QString Room::retryMessage(const QString& txnId) return d->doSendEvent(it->event()); } +// Using a function defers actual tr() invocation to the moment when +// translations are initialised +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(), @@ -1719,10 +2136,10 @@ void Room::discardMessage(const QString& txnId) const auto& transferIt = d->fileTransfers.find(txnId); if (transferIt != d->fileTransfers.end()) { Q_ASSERT(transferIt->isUpload); - if (isJobRunning(transferIt->job)) { + 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 @@ -1762,57 +2179,81 @@ 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"; - } - } - }); - 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(); - } - } + const auto& transferJob = fileTransfers.value(txnId).job; + connect(q, &Room::fileTransferCompleted, transferJob, + [this, txnId](const QString& tId, const QUrl&, + const FileSourceInfo& fileMetadata) { + if (tId != txnId) + return; + + const auto it = q->findPendingEvent(txnId); + if (it != unsyncedEvents.end()) { + it->setFileUploaded(fileMetadata); + 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" << getUrlFromSourceInfo(fileMetadata) + << "but the event referring to it was " + "cancelled"; + } + }); + 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)); @@ -1824,34 +2265,45 @@ QString Room::postJson(const QString& matrixType, return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent)); } -SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const +SetRoomStateWithKeyJob* Room::setState(const StateEvent& evt) +{ + return setState(evt.matrixType(), evt.stateKey(), evt.contentJson()); +} + +SetRoomStateWithKeyJob* Room::setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson) { - return d->requestSetState(evt); + return d->requestSetState(evtType, stateKey, contentJson); } void Room::setName(const QString& newName) { - d->requestSetState<RoomNameEvent>(newName); + setState<RoomNameEvent>(newName); } void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState<RoomCanonicalAliasEvent>(newAlias, altAliases()); + setState<RoomCanonicalAliasEvent>(newAlias, altAliases()); } +void Room::setPinnedEvents(const QStringList& events) +{ + setState<RoomPinnedEvent>(events); +} void Room::setLocalAliases(const QStringList& aliases) { - d->requestSetState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases); + setState<RoomCanonicalAliasEvent>(canonicalAlias(), aliases); } void Room::setTopic(const QString& newTopic) { - d->requestSetState<RoomTopicEvent>(newTopic); + setState<RoomTopicEvent>(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) { - if (le->type() != re->type()) + if (le->metaType() != re->metaType()) return false; if (!re->id().isEmpty()) @@ -1902,11 +2354,12 @@ void Room::sendCallCandidates(const QString& callId, d->sendEvent<CallCandidatesEvent>(callId, candidates); } -void Room::answerCall(const QString& callId, const int lifetime, +void Room::answerCall(const QString& callId, [[maybe_unused]] int lifetime, const QString& sdp) { - Q_ASSERT(supportsCalls()); - d->sendEvent<CallAnswerEvent>(callId, lifetime, sdp); + qCWarning(MAIN) << "To client developer: drop lifetime parameter from " + "Room::answerCall(), it is no more accepted"; + answerCall(callId, sdp); } void Room::answerCall(const QString& callId, const QString& sdp) @@ -1921,17 +2374,20 @@ void Room::hangupCall(const QString& callId) d->sendEvent<CallHangupEvent>(callId); } -void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); } +void Room::getPreviousContent(int limit, const QString& filter) +{ + d->getPreviousContent(limit, filter); +} -void Room::Private::getPreviousContent(int limit) +void Room::Private::getPreviousContent(int limit, const QString &filter) { - if (isJobRunning(eventsHistoryJob)) + if (isJobPending(eventsHistoryJob)) return; - eventsHistoryJob = - connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit); + eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, "b", prevBatch, + "", limit, filter); emit q->eventsHistoryJobChanged(); - connect(eventsHistoryJob, &BaseJob::success, q, [=] { + connect(eventsHistoryJob, &BaseJob::success, q, [this] { prevBatch = eventsHistoryJob->end(); addHistoricalMessageEvents(eventsHistoryJob->chunk()); }); @@ -1950,12 +2406,6 @@ LeaveRoomJob* Room::leaveRoom() return connection()->leaveRoom(this); } -SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId, - const RoomMemberEvent& event) const -{ - return d->requestSetState<RoomMemberEvent>(memberId, event.content()); -} - void Room::kickMember(const QString& memberId, const QString& reason) { connection()->callApi<KickJob>(id(), memberId, reason); @@ -1983,18 +2433,35 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, Q_ASSERT_X(localFilename.isLocalFile(), __FUNCTION__, "localFilename should point at a local file"); auto fileName = localFilename.toLocalFile(); + FileSourceInfo fileMetadata; +#ifdef Quotient_E2EE_ENABLED + QTemporaryFile tempFile; + if (usesEncryption()) { + tempFile.open(); + QFile file(localFilename.toLocalFile()); + file.open(QFile::ReadOnly); + QByteArray data; + std::tie(fileMetadata, data) = encryptFile(file.readAll()); + tempFile.write(data); + tempFile.close(); + fileName = QFileInfo(tempFile).absoluteFilePath(); + } +#endif auto job = connection()->uploadFile(fileName, overrideContentType); - if (isJobRunning(job)) { + if (isJobPending(job)) { d->fileTransfers[id] = { job, fileName, true }; connect(job, &BaseJob::uploadProgress, this, [this, id](qint64 sent, qint64 total) { d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this, id, localFilename, job] { - d->fileTransfers[id].status = FileTransferInfo::Completed; - emit fileTransferCompleted(id, localFilename, job->contentUri()); - }); + connect(job, &BaseJob::success, this, + [this, id, localFilename, job, fileMetadata]() mutable { + // The lambda is mutable to change encryptedFileMetadata + d->fileTransfers[id].status = FileTransferInfo::Completed; + setUrlInSourceInfo(fileMetadata, QUrl(job->contentUri())); + emit fileTransferCompleted(id, localFilename, fileMetadata); + }); connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, id, job->errorString())); emit newFileTransfer(id, localFilename); @@ -2027,11 +2494,11 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) << "has an empty or malformed mxc URL; won't download"; return; } - const auto fileUrl = fileInfo->url; + const auto fileUrl = fileInfo->url(); auto filePath = localFilename.toLocalFile(); if (filePath.isEmpty()) { // Setup default file path filePath = - fileInfo->url.path().mid(1) % '_' % d->fileNameToDownload(event); + fileInfo->url().path().mid(1) % '_' % d->fileNameToDownload(event); if (filePath.size() > 200) // If too long, elide in the middle filePath.replace(128, filePath.size() - 192, "---"); @@ -2039,8 +2506,18 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) filePath = QDir::tempPath() % '/' % filePath; qDebug(MAIN) << "File path:" << filePath; } - auto job = connection()->downloadFile(fileUrl, filePath); - if (isJobRunning(job)) { + DownloadFileJob *job = nullptr; +#ifdef Quotient_E2EE_ENABLED + if (auto* fileMetadata = + std::get_if<EncryptedFileMetadata>(&fileInfo->source)) { + job = connection()->downloadFile(fileUrl, *fileMetadata, filePath); + } else { +#endif + job = connection()->downloadFile(fileUrl, filePath); +#ifdef Quotient_E2EE_ENABLED + } +#endif + if (isJobPending(job)) { // If there was a previous transfer (completed or failed), overwrite it. d->fileTransfers[eventId] = { job, job->targetFileName() }; connect(job, &BaseJob::downloadProgress, this, @@ -2056,22 +2533,23 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) connect(job, &BaseJob::failure, this, std::bind(&Private::failedTransfer, d, eventId, job->errorString())); + emit newFileTransfer(eventId, localFilename); } else d->failedTransfer(eventId); } 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 (isJobRunning(it->job)) + 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 @@ -2099,6 +2577,26 @@ void Room::Private::dropDuplicateEvents(RoomEvents& events) const events.erase(dupsBegin, events.end()); } +void Room::Private::decryptIncomingEvents(RoomEvents& events) +{ +#ifdef Quotient_E2EE_ENABLED + QElapsedTimer et; + et.start(); + size_t totalDecrypted = 0; + for (auto& eptr : events) + if (const auto& eeptr = eventCast<EncryptedEvent>(eptr)) { + if (auto decrypted = q->decryptMessage(*eeptr)) { + ++totalDecrypted; + auto&& oldEvent = exchange(eptr, move(decrypted)); + eptr->setOriginalEvent(::move(oldEvent)); + } else + undecryptedEvents[eeptr->sessionId()] += eeptr->id(); + } + if (totalDecrypted > 5 || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) << "Decrypted" << totalDecrypted << "events in" << et; +#endif +} + /** Make a redacted event * * This applies the redaction procedure as defined by the CS API specification @@ -2108,10 +2606,10 @@ 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, - QStringLiteral("room_id"), QStringLiteral("sender"), StateKeyKey, + static const QStringList keepKeys { + EventIdKey, TypeKey, RoomIdKey, SenderKey, StateKeyKey, QStringLiteral("hashes"), QStringLiteral("signatures"), QStringLiteral("depth"), QStringLiteral("prev_events"), QStringLiteral("prev_state"), QStringLiteral("auth_events"), @@ -2119,18 +2617,18 @@ RoomEventPtr makeRedacted(const RoomEvent& target, QStringLiteral("membership") }; // clang-format on - std::vector<std::pair<Event::Type, QStringList>> keepContentKeysMap { - { RoomMemberEvent::typeId(), { QStringLiteral("membership") } }, - { RoomCreateEvent::typeId(), { QStringLiteral("creator") } }, - { RoomPowerLevelsEvent::typeId(), + 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") } } - // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } - // , { RoomHistoryVisibility::typeId(), - // { QStringLiteral("history_visibility") } } + // TODO: Replace with RoomJoinRules::TypeId etc. once available + { "m.room.join_rules"_ls, { QStringLiteral("join_rule") } }, + { "m.room.history_visibility"_ls, + { QStringLiteral("history_visibility") } } }; for (auto it = originalJson.begin(); it != originalJson.end();) { if (!keepKeys.contains(it.key())) @@ -2139,9 +2637,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 { @@ -2155,7 +2653,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); @@ -2183,12 +2681,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); if (oldEvent->isStateEvent()) { - const StateEventKey evtKey { oldEvent->matrixType(), - oldEvent->stateKey() }; - Q_ASSERT(currentState.contains(evtKey)); - if (currentState.value(evtKey) == oldEvent.get()) { - Q_ASSERT(ti.index() >= 0); // Historical states can't be in - // currentState + // Check whether the old event was a part of current state; if it was, + // update the current state to the redacted event object. + const auto currentStateEvt = + currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); + Q_ASSERT(currentStateEvt); + if (currentStateEvt == oldEvent.get()) { + // Historical states can't be in currentState + Q_ASSERT(ti.index() >= 0); qCDebug(STATE).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); @@ -2200,8 +2700,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) { const auto& targetEvtId = reaction->relation().eventId; - const auto lookupKey = - qMakePair(targetEvtId, EventRelation::Annotation()); + const std::pair lookupKey { targetEvtId, EventRelation::AnnotationType }; if (relations.contains(lookupKey)) { relations[lookupKey].removeOne(reaction); emit q->updatedEvent(targetEvtId); @@ -2209,6 +2708,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + // By now, all references to oldEvent must have been updated to ti.event() return true; } @@ -2220,8 +2720,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(); @@ -2281,10 +2786,13 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) { dropDuplicateEvents(events); if (events.empty()) - return Change::NoChange; + return Change::None; + + decryptIncomingEvents(events); 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 @@ -2334,7 +2842,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); @@ -2391,7 +2899,7 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (q->supportsCalls()) for (auto it = from; it != syncEdge(); ++it) - if (const auto* evt = it->viewAs<CallEventBase>()) + if (const auto* evt = it->viewAs<CallEvent>()) emit q->callEvent(q, evt); if (totalInserted > 0) { @@ -2407,23 +2915,16 @@ 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 receipt 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, calling - // setLastDisplayedEventId() - to promote their read receipts over - // the new message events. - if (auto* const firstWriter = q->user((*from)->senderId())) { - setLastReadReceipt(firstWriter, rev_iter_t(from + 1)); - if (firstWriter == q->localUser() && q->readMarker().base() == from) { - // 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. - roomChanges |= - setFullyReadMarker(lastReadEventIds.value(firstWriter)); - } - } - roomChanges |= updateUnreadCount(timeline.crbegin(), rev_iter_t(from)); + roomChanges |= updateStats(timeline.crbegin(), rev_iter_t(from)); + + // 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); @@ -2435,14 +2936,17 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) { - QElapsedTimer et; - et.start(); const auto timelineSize = timeline.size(); dropDuplicateEvents(events); if (events.empty()) return; + decryptIncomingEvents(events); + + QElapsedTimer et; + et.start(); + 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 @@ -2450,8 +2954,8 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) for (const auto& eptr : events) { const auto& e = *eptr; if (e.isStateEvent() - && !currentState.contains({ e.matrixType(), e.stateKey() })) { - q->processStateEvent(e); + && !currentState.contains(e.matrixType(), e.stateKey())) { + changes |= q->processStateEvent(e); } } @@ -2471,108 +2975,133 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) emit q->updatedEvent(relation.eventId); } } - if (updateUnreadCount(from, historyEdge()) != NoChange) - connection->saveRoomState(q); - - // When there are no unread messages and the read marker is within the - // known timeline, unreadMessages == -1 - // (see https://github.com/quotient-im/libQuotient/wiki/unread_count). - Q_ASSERT(unreadMessages != 0 || q->readMarker() == historyEdge()); - Q_ASSERT(timeline.size() == timelineSize + insertedSize); if (insertedSize > 9 || et.nsecsElapsed() >= profilerMinNsecs()) 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 Change::NoChange; - - auto* const sender = user(e.senderId()); - if (!sender) { - qCWarning(MAIN) << "State event" << e.id() - << "is invalid and won't be processed"; - return Change::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 - // a stub if a value is not found, and what's needed here is a "real" event - // or nullptr. + // to it, anticipating a change further in the function. auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change - const auto oldRme = static_cast<const RoomMemberEvent*>(curStateEvent); - visit(e, [this, &oldRme](const RoomMemberEvent& rme) { - auto* const u = user(rme.userId()); - if (!u) { // Invalid user id? - qCWarning(MAIN) - << "Could not get a user object for" << rme.userId(); - return; - } - // TODO: remove along with User::processEvent() in 0.7 - const auto prevMembership = oldRme ? oldRme->membership() - : MembershipType::Leave; - u->processEvent(rme, this, oldRme == nullptr); - - switch (prevMembership) { - case MembershipType::Invite: - if (rme.membership() != prevMembership) { - d->usersInvited.removeOne(u); - Q_ASSERT(!d->usersInvited.contains(u)); + // clang-format off + const bool proceed = switchOnType(e + , [this, curStateEvent](const RoomMemberEvent& rme) { + // clang-format on + auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent); + auto* u = user(rme.userId()); + if (!u) { // Some terribly malformed user id? + qCCritical(MAIN) << "Could not get a user object for" + << rme.userId(); + return false; // Stay low and hope for the best... } - break; - case MembershipType::Join: - switch (rme.membership()) { - case MembershipType::Join: // rename/avatar change or no-op - if (rme.displayName() != oldRme->displayName()) { - emit memberAboutToRename(u, rme.displayName()); + const auto prevMembership = oldRme ? oldRme->membership() + : Membership::Leave; + switch (prevMembership) { + case Membership::Invite: + if (rme.membership() != prevMembership) { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case Membership::Join: + if (rme.membership() == Membership::Join) { + // rename/avatar change or no-op + if (rme.newDisplayName()) { + emit memberAboutToRename(u, *rme.newDisplayName()); + d->removeMemberFromMap(u); + } + if (!rme.newDisplayName() && !rme.newAvatarUrl()) { + qCWarning(MEMBERS) + << "No-op membership event for" << rme.userId() + << "- retaining the state"; + qCWarning(MEMBERS) << "The event dump:" << rme; + return false; + } + } else { + if (rme.membership() == Membership::Invite) + qCWarning(MAIN) + << "Membership change from Join to Invite:" << rme; + // whatever the new membership, it's no more Join d->removeMemberFromMap(u); + emit userRemoved(u); } break; - case MembershipType::Invite: - qCWarning(MAIN) << "Membership change from Join to Invite:" - << rme; - [[fallthrough]]; - default: // whatever the new membership, it's no more Join - d->removeMemberFromMap(u); - emit userRemoved(u); + 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 Membership::Undefined: + ; // A warning will be dropped in the post-processing block below } - break; - default: - if (rme.membership() == MembershipType::Invite - || rme.membership() == MembershipType::Join) { - d->membersLeft.removeOne(u); - Q_ASSERT(!d->membersLeft.contains(u)); + return true; + // clang-format off + } + , [this, curStateEvent]( const EncryptionEvent& ee) { + // clang-format on + auto* oldEncEvt = + static_cast<const EncryptionEvent*>(curStateEvent); + if (ee.algorithm().isEmpty()) { + qWarning(STATE) + << "The encryption event for room" << objectName() + << "doesn't have 'algorithm' specified - ignoring"; + return false; } + if (oldEncEvt + && oldEncEvt->encryption() != EncryptionType::Undefined) { + qCWarning(STATE) << "The room is already encrypted but a new" + " room encryption event arrived - ignoring"; + return false; + } + return true; + // clang-format off } - }); + , true); // By default, go forward with the state change + // clang-format on + 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 = - std::exchange(curStateEvent, static_cast<const StateEventBase*>(&e)); + std::exchange(curStateEvent, static_cast<const StateEvent*>(&e)); Q_ASSERT(!oldStateEvent || (oldStateEvent->matrixType() == e.matrixType() && oldStateEvent->stateKey() == e.stateKey())); - if (!is<RoomMemberEvent>(e)) // Room member events are too numerous + if (is<RoomMemberEvent>(e)) + qCDebug(MEMBERS) << "Updated room member state:" << e; + else qCDebug(STATE) << "Updated room state:" << e; // Update internal structures as per the change and work out the return value // clang-format off - return visit(e + const auto result = switchOnType(e , [] (const RoomNameEvent&) { - return NameChange; - } - , [] (const RoomAliasesEvent&) { - return NoChange; // This event has been removed by MSC2432 + 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(); @@ -2584,73 +3113,68 @@ 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&) { + emit pinnedEventsChanged(); + return Change::Other; + } , [] (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,oldRme,sender] (const RoomMemberEvent& evt) { + , [this,oldStateEvent] (const RoomMemberEvent& evt) { // clang-format on auto* u = user(evt.userId()); - if (!u) - return NoChange; // Already warned earlier - // TODO: remove in 0.7 - u->processEvent(evt, this, oldRme == nullptr); - - const auto prevMembership = oldRme ? oldRme->membership() - : MembershipType::Leave; + const auto* oldMemberEvent = + static_cast<const RoomMemberEvent*>(oldStateEvent); + const auto prevMembership = oldMemberEvent + ? oldMemberEvent->membership() + : 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 if (oldRme->displayName() != evt.displayName()) { - d->insertMemberIntoMap(u); - emit memberRenamed(u); + } else { + if (evt.newDisplayName()) { + d->insertMemberIntoMap(u); + emit memberRenamed(u); + } + if (evt.newAvatarUrl()) + 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, sender); + 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 Membership::Undefined: + qCWarning(MEMBERS) << "Ignored undefined membership type"; } - return MembersChange; + return Change::Members; // clang-format off } - , [this, oldEncEvt = static_cast<const EncryptionEvent*>(oldStateEvent)]( - const EncryptionEvent& ee) { - // clang-format on - if (ee.algorithm().isEmpty()) { - qWarning(STATE) - << "The encryption event for room" << objectName() - << "doesn't have 'algorithm' specified - ignoring"; - return NoChange; - } - if (oldEncEvt - && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { - qCWarning(STATE) << "The room is already encrypted but a new" - " room encryption event arrived - ignoring"; - return NoChange; - } + , [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; - // clang-format off + return Change::Other; } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); @@ -2666,80 +3190,93 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return true; }); - return OtherChange; + return Change::Other; + // clang-format off } - ); + , Change::Other); // clang-format on + 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 QString& userId : qAsConst(evt->users())) { - auto* const u = user(userId); - if (memberJoinState(u) == JoinState::Join) - d->usersTyping.append(u); - } - if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) - << "Processing typing events from" << evt->users().size() - << "user(s) in" << objectName() << "took" << et; - emit typingChanged(); - } - if (auto* evt = eventCast<ReceiptEvent>(event)) { - int totalReceipts = 0; - for (const auto& p : qAsConst(evt->eventsWithReceipts())) { - totalReceipts += p.receipts.size(); - { - if (p.receipts.size() == 1) - qCDebug(EPHEMERAL) - << objectName() << "received a read receipt for" - << p.evtId << "from" << p.receipts[0].userId; - else - qCDebug(EPHEMERAL) - << objectName() << "received read receipts for" - << p.evtId << "from" << p.receipts.size() << "users"; - } - const auto newMarker = findInTimeline(p.evtId); - if (newMarker == historyEdge()) - qCDebug(EPHEMERAL) << "Event of the read receipt(s) is not " - "found; saving them anyway"; - for (const Receipt& r : p.receipts) - if (auto* const u = user(r.userId); - memberJoinState(u) == JoinState::Join) { - // 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. - d->setLastReadReceipt(u, newMarker, p.evtId); + switchOnType(*event, + [this, &et](const TypingEvent& evt) { + const auto& users = evt.users(); + d->usersTyping.clear(); + d->usersTyping.reserve(users.size()); // Assume all are members + for (const auto& userId : users) + if (isMember(userId)) + d->usersTyping.append(user(userId)); + + if (d->usersTyping.size() > 3 + || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) + << "Processing typing events from" << users.size() + << "user(s) in" << objectName() << "took" << et; + emit typingChanged(); + }, + [this, &changes, &et](const ReceiptEvent& evt) { + const auto& receiptsJson = evt.contentJson(); + QVector<QString> updatedUserIds; + // Most often (especially for bigger batches), receipts are + // scattered across events (an anecdotal evidence showed 1.2-1.3 + // receipts per event on average). + updatedUserIds.reserve(receiptsJson.size() * 2); + for (auto eventIt = receiptsJson.begin(); + eventIt != receiptsJson.end(); ++eventIt) { + const auto evtId = eventIt.key(); + const auto newMarker = findInTimeline(evtId); + if (newMarker == historyEdge()) + qDebug(EPHEMERAL) + << "Event" << evtId + << "is not found; saving read receipt(s) anyway"; + const auto reads = + eventIt.value().toObject().value("m.read"_ls).toObject(); + for (auto userIt = reads.begin(); userIt != reads.end(); + ++userIt) { + ReadReceipt rr{ evtId, + fromJson<QDateTime>( + userIt->toObject().value("ts"_ls)) }; + const auto userId = userIt.key(); + if (userId == connection()->userId()) { + // Local user is special, and will get a signal about + // its read receipt separately from (and before) a + // signal on everybody else. No particular reason, just + // less cumbersome code. + changes |= d->setLocalLastReadReceipt(newMarker, rr); + } else if (d->setLastReadReceipt(userId, newMarker, rr)) { + changes |= Change::Other; + updatedUserIds.push_back(userId); + } } - } - if (evt->eventsWithReceipts().size() > 3 || totalReceipts > 10 - || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "Processing" << totalReceipts << "receipt(s) on" - << evt->eventsWithReceipts().size() - << "event(s) in" << objectName() << "took" << et; - } + } + if (updatedUserIds.size() > 10 + || et.nsecsElapsed() >= profilerMinNsecs()) + qDebug(PROFILER) + << "Processing" << updatedUserIds.size() + << "non-local receipt(s) on" << receiptsJson.size() + << "event(s) in" << objectName() << "took" << et; + if (!updatedUserIds.empty()) + emit lastReadEventChanged(updatedUserIds); + }); 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<const ReadMarkerEvent>(event)) - changes |= d->setFullyReadMarker(evt->event_id()); + changes |= d->setFullyReadMarker(evt->eventId()); // For all account data events auto& currentData = d->accountData[event->matrixType()]; @@ -2751,7 +3288,10 @@ Room::Changes Room::processAccountDataEvent(EventPtr&& event) qCDebug(STATE) << "Updated account data of type" << currentData->matrixType(); emit accountDataChanged(currentData->matrixType()); - changes |= 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 changes; } @@ -2833,7 +3373,7 @@ QString Room::Private::calculateDisplayname() const shortlist = buildShortlist(membersLeft); QStringList names; - for (auto u : shortlist) { + for (const auto* u : shortlist) { if (u == nullptr || isLocalUser(u)) break; // Only disambiguate if the room is not empty @@ -2921,41 +3461,24 @@ QJsonObject Room::Private::toJson() const { QStringLiteral("events"), accountDataEvents } }); } - if (const auto& readReceiptEventId = lastReadEventIds.value(q->localUser()); - !readReceiptEventId.isEmpty()) // + if (const auto& readReceipt = q->lastReadReceipt(connection->userId()); + !readReceipt.eventId.isEmpty()) // { - // Okay, that's a mouthful; but basically, it's simply placing an m.read - // event in the 'ephemeral' section of the cached sync payload. - // See also receiptevent.* and m.read example in the spec. - // Only the local user's read receipt is saved - others' are really - // considered ephemeral but this one is useful in understanding where - // the user is in the timeline before any history is loaded. result.insert( QStringLiteral("ephemeral"), QJsonObject { { QStringLiteral("events"), - QJsonArray { QJsonObject { - { TypeKey, ReceiptEvent::matrixTypeId() }, - { ContentKey, - QJsonObject { - { readReceiptEventId, - QJsonObject { - { QStringLiteral("m.read"), - QJsonObject { - { connection->userId(), - QJsonObject {} } } } } } } } } } } }); + QJsonArray { ReceiptEvent({ { readReceipt.eventId, + { { connection->userId(), + readReceipt.timestamp } } } }) + .fullJson() } } }); } - 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); + result.insert(UnreadNotificationsKey, + QJsonObject { { PartiallyReadCountKey, + countFromStats(partiallyReadStats) }, + { HighlightCountKey, serverHighlightCount } }); + result.insert(NewUnreadCountKey, countFromStats(unreadStats)); if (et.elapsed() > 30) qCDebug(PROFILER) << "Room::toJson() for" << q->objectName() << "took" @@ -2970,15 +3493,28 @@ MemberSorter Room::memberSorter() const { return MemberSorter(this); } bool MemberSorter::operator()(User* u1, User* u2) const { - return operator()(u1, room->roomMembername(u2)); + 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->roomMembername(u1); + 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>(EncryptionType::MegolmV1AesSha2); +} @@ -1,26 +1,18 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name> +// SPDX-FileCopyrightText: 2017 Marius Gripsgard <marius@ubports.com> +// SPDX-FileCopyrightText: 2018 Josip Delic <delijati@googlemail.com> +// SPDX-FileCopyrightText: 2018 Black Hat <bhat@encom.eu.org> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-FileCopyrightText: 2020 Ram Nad <ramnad1999@gmail.com> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "connection.h" +#include "roomstateview.h" #include "eventitem.h" -#include "joinstate.h" +#include "quotient_common.h" #include "csapi/message_pagination.h" @@ -30,6 +22,7 @@ #include "events/roommessageevent.h" #include "events/roomcreateevent.h" #include "events/roomtombstoneevent.h" +#include "events/eventrelation.h" #include <QtCore/QJsonObject> #include <QtGui/QImage> @@ -54,7 +47,7 @@ class RedactEventJob; * This is specifically tuned to work with QML exposing all traits as * Q_PROPERTY values. */ -class FileTransferInfo { +class QUOTIENT_API FileTransferInfo { Q_GADGET Q_PROPERTY(bool isUpload MEMBER isUpload CONSTANT) Q_PROPERTY(bool active READ active CONSTANT) @@ -80,7 +73,46 @@ public: bool failed() const { return status == Failed; } }; -class Room : public QObject { +//! \brief Data structure for a room member's read receipt +//! \sa Room::lastReadReceipt +class QUOTIENT_API 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(Type) + + Type type = None; + +private: + Q_GADGET + Q_PROPERTY(Type type MEMBER type CONSTANT) +}; + +class QUOTIENT_API Room : public QObject { Q_OBJECT Q_PROPERTY(Connection* connection READ connection CONSTANT) Q_PROPERTY(User* localUser READ localUser CONSTANT) @@ -94,6 +126,9 @@ class Room : public QObject { Q_PROPERTY(QStringList altAliases READ altAliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) + Q_PROPERTY(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) @@ -101,8 +136,7 @@ class Room : public QObject { Q_PROPERTY(bool usesEncryption READ usesEncryption NOTIFY encryption) Q_PROPERTY(int timelineSize READ timelineSize NOTIFY addedMessages) - Q_PROPERTY(QStringList memberNames READ memberNames NOTIFY memberListChanged) - Q_PROPERTY(int memberCount READ memberCount NOTIFY memberListChanged) + Q_PROPERTY(QStringList memberNames READ safeMemberNames 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) @@ -113,21 +147,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) @@ -139,26 +180,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; @@ -188,18 +252,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; @@ -209,13 +270,14 @@ public: QList<User*> membersLeft() const; Q_INVOKABLE QList<Quotient::User*> users() const; + Q_DECL_DEPRECATED_X("Use safeMemberNames() or htmlSafeMemberNames() instead") // QStringList memberNames() const; - [[deprecated("Use joinedCount(), invitedCount(), totalMemberCount()")]] - int memberCount() const; + QStringList safeMemberNames() const; + QStringList htmlSafeMemberNames() const; int timelineSize() const; bool usesEncryption() const; RoomEventPtr decryptMessage(const EncryptedEvent& encryptedEvent); - void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderKey); + void handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, const QString& senderId, const QString& olmSessionId); int joinedCount() const; int invitedCount() const; int totalMemberCount() const; @@ -251,31 +313,58 @@ public: /** * \brief Check the join state of a given user in this room * - * \note Banned and invited users are not tracked for now (Leave + * \note Banned and invited users are not tracked separately for now (Leave * will be returned for them). * * \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; - /** - * Get a disambiguated name for a given user in - * the context of the room - */ + //! \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 + Q_DECL_DEPRECATED_X("Use safeMemberName() instead") Q_INVOKABLE QString roomMembername(const Quotient::User* u) const; - /** - * Get a disambiguated name for a user with this id in - * the context of the room - */ + //! \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; - /** Get a display-safe member name in the context of this room + /*! + * \brief Get a disambiguated name for the member with the given MXID + * + * This function should only be used for non-UI code; consider using + * safeMemberName() or htmlSafeMemberName() for displayed strings. + */ + Q_INVOKABLE QString disambiguatedMemberName(const QString& mxId) const; + + /*! Get a display-safe member name in the context of this room * - * Display-safe means HTML-safe + without RLO/LRO markers + * Display-safe means disambiguated and without RLO/LRO markers * (see https://github.com/quotient-im/Quaternion/issues/545). */ Q_INVOKABLE QString safeMemberName(const QString& userId) const; + /*! Get an HTML-safe member name in the context of this room + * + * This function adds HTML escaping on top of safeMemberName() safeguards. + */ + Q_INVOKABLE QString htmlSafeMemberName(const QString& userId) const; + + //! \brief Get an avatar for the member with the given MXID + QUrl memberAvatarUrl(const QString& mxId) const; + const Timeline& messageEvents() const; const PendingEvents& pendingEvents() const; @@ -296,8 +385,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 @@ -309,14 +396,12 @@ public: PendingEvents::const_iterator findPendingEvent(const QString& txnId) const; const RelatedEvents relatedEvents(const QString& evtId, - const char* relType) const; + EventRelation::reltypeid_t relType) const; const RelatedEvents relatedEvents(const RoomEvent& evt, - const char* relType) const; + EventRelation::reltypeid_t relType) const; - const RoomCreateEvent* creation() const - { return getCurrentState<RoomCreateEvent>(); } - const RoomTombstoneEvent* tombstone() const - { return getCurrentState<RoomTombstoneEvent>(); } + const RoomCreateEvent* creation() const; + const RoomTombstoneEvent* tombstone() const; bool displayed() const; /// Mark the room as currently displayed to the user @@ -336,62 +421,223 @@ public: void setLastDisplayedEventId(const QString& eventId); void setLastDisplayedEvent(TimelineItem::index_t index); - /*! \brief Obtain a read receipt of any user - * - * Since 0.6.8, there's an important difference between the single-argument - * and the zero-argument overloads of this function: a call with an argument - * returns the last _read receipt_ position (for any room member) while - * a call without arguments returns the last _fully read_ position. - * This is due to API stability guarantees; 0.7 will have distinctly named - * methods to return read receipts and the fully read marker. - */ + //! \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 - * - * \sa the description for the single-argument overload of this function - */ + //! \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 + //! \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. If the fully read marker is within - * the displayed viewport (between firstDisplayedMarker() and - * lastDisplayedMarker()) then it is advanced as well. - * - * 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 @@ -427,12 +673,12 @@ public: * actions on the room to its predecessors and successors. */ enum ActionScope { - ThisRoomOnly, //< Do not apply to predecessors and successors - WithinSameState, //< Apply to predecessors and successors in the same - //< state as the current one - OmitLeftState, //< Apply to all reachable predecessors and successors - //< except those in Leave state - WholeSequence //< Apply to all reachable predecessors and successors + ThisRoomOnly, ///< Do not apply to predecessors and successors + WithinSameState, ///< Apply to predecessors and successors in the same + ///< state as the current one + OmitLeftState, ///< Apply to all reachable predecessors and successors + ///< except those in Leave state + WholeSequence ///< Apply to all reachable predecessors and successors }; /** Overwrite the room's tags @@ -461,6 +707,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; @@ -506,39 +755,54 @@ public: /*! This method returns a (potentially empty) state event corresponding * to the pair of event type \p evtType and state key \p stateKey. */ - Q_INVOKABLE const Quotient::StateEventBase* - getCurrentState(const QString& evtType, const QString& stateKey = {}) const; + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // + const StateEvent* getCurrentState(const QString& evtType, + const QString& stateKey = {}) const; /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of * its Matrix name. */ template <typename EvT> + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // const EvT* getCurrentState(const QString& stateKey = {}) const { - const auto* evt = - eventCast<const EvT>(getCurrentState(EvT::matrixTypeId(), stateKey)); + QT_IGNORE_DEPRECATIONS(const auto* evt = eventCast<const EvT>( + getCurrentState(EvT::TypeId, stateKey));) Q_ASSERT(evt); - Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId() + Q_ASSERT(evt->matrixType() == EvT::TypeId && evt->stateKey() == stateKey); return evt; } - /// Set a state event of the given type with the given arguments - /*! This typesafe overload attempts to send a state event with the type - * \p EvT and the content defined by \p args. Specifically, the function - * creates a temporary object of type \p EvT passing \p args to - * the constructor, and sends a request to the homeserver using - * the Matrix event type defined by \p EvT and the event content produced - * via EvT::contentJson(). - */ + /// \brief Get the current room state + RoomStateView currentState() const; + + //! Send a request to update the room state with the given event + SetRoomStateWithKeyJob* setState(const StateEvent& evt); + + //! \brief Set a state event of the given type with the given arguments + //! + //! This typesafe overload attempts to send a state event with the type + //! \p EvT and the content defined by \p args. Specifically, the function + //! constructs a temporary object of type \p EvT with its content + //! list-initialised from \p args, and sends a request to the homeserver + //! using the Matrix event type defined by \p EvT and the event content + //! produced via EvT::contentJson(). + //! + //! \note This call is not suitable for events that assume non-empty + //! stateKey, such as member events; for those you have to create + //! a temporary event object yourself and use the setState() overload + //! that accepts StateEvent const-ref. template <typename EvT, typename... ArgTs> - auto setState(ArgTs&&... args) const + auto setState(ArgTs&&... args) { return setState(EvT(std::forward<ArgTs>(args)...)); } -public slots: +public Q_SLOTS: /** Check whether the room should be upgraded */ void checkVersion(); @@ -549,8 +813,13 @@ public 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 @@ -562,10 +831,13 @@ public slots: QString retryMessage(const QString& txnId); void discardMessage(const QString& txnId); - /// Send a request to update the room state with the given event - SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; + //! Send a request to update the room state based on freeform inputs + SetRoomStateWithKeyJob* setState(const QString& evtType, + const QString& stateKey, + const QJsonObject& contentJson); void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); + void setPinnedEvents(const QStringList& events); /// Set room aliases on the user's current server void setLocalAliases(const QStringList& aliases); void setTopic(const QString& newTopic); @@ -573,13 +845,10 @@ public slots: /// You shouldn't normally call this method; it's here for debugging void refreshDisplayName(); - void getPreviousContent(int limit = 10); + void getPreviousContent(int limit = 10, const QString &filter = {}); void inviteToRoom(const QString& memberId); LeaveRoomJob* leaveRoom(); - /// \deprecated - use setState() instead") - SetRoomStateWithKeyJob* setMemberState(const QString& memberId, - const RoomMemberEvent& event) const; void kickMember(const QString& memberId, const QString& reason = {}); void ban(const QString& userId, const QString& reason = {}); void unban(const QString& userId); @@ -591,7 +860,12 @@ public slots: void downloadFile(const QString& eventId, const QUrl& localFilename = {}); void cancelFileTransfer(const QString& id); - /// Mark the bottommost message in the room as fully 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) @@ -600,12 +874,19 @@ public slots: void inviteCall(const QString& callId, const int lifetime, const QString& sdp); void sendCallCandidates(const QString& callId, const QJsonArray& candidates); - void answerCall(const QString& callId, const int lifetime, - const QString& sdp); + [[deprecated("Lifetime argument is no more passed; " + "use 2-arg Room::answerCall() instead")]] + void answerCall(const QString& callId, int lifetime, const QString& sdp); void answerCall(const QString& callId, const QString& sdp); void hangupCall(const QString& callId); -signals: + /** + * Activates encryption for this room. + * Warning: Cannot be undone + */ + void activateEncryption(); + +Q_SIGNALS: /// Initial set of state events has been loaded /** * The initial set is what comes from the initial sync for the room. @@ -616,11 +897,11 @@ 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 @@ -664,12 +945,14 @@ signals: void namesChanged(Quotient::Room* room); void displaynameAboutToChange(Quotient::Room* room); void displaynameChanged(Quotient::Room* room, QString oldName); + void pinnedEventsChanged(); void topicChanged(); void avatarChanged(); void userAdded(Quotient::User* user); void userRemoved(Quotient::User* user); void memberAboutToRename(Quotient::User* user, QString newName); void memberRenamed(Quotient::User* user); + void memberAvatarChanged(Quotient::User* user); /// The list of members has changed /** Emitted no more than once per sync, this is a good signal to * for cases when some action should be done upon any change in @@ -687,17 +970,26 @@ 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(); - void lastReadEventChanged(Quotient::User* user); + //! The event the m.read receipt points to has changed for the listed users + //! \sa lastReadReceipt + void lastReadEventChanged(QVector<QString> userIds); + 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); @@ -710,9 +1002,11 @@ signals: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl); + void fileTransferCompleted(QString id, QUrl localFile, + FileSourceInfo fileMetadata); 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); @@ -738,6 +1032,7 @@ protected: {} virtual QJsonObject toJson() const; virtual void updateData(SyncRoomData&& data, bool fromCache = false); + virtual Notification checkForNotifications(const TimelineItem& ti); private: friend class Connection; @@ -751,12 +1046,12 @@ private: void setJoinState(JoinState state); }; -class MemberSorter { +class QUOTIENT_API MemberSorter { 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 @@ -769,4 +1064,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/roomstateview.cpp b/lib/roomstateview.cpp new file mode 100644 index 00000000..be0f7c6c --- /dev/null +++ b/lib/roomstateview.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roomstateview.h" + +using namespace Quotient; + +const StateEvent* RoomStateView::get(const QString& evtType, + const QString& stateKey) const +{ + return value({ evtType, stateKey }); +} + +bool RoomStateView::contains(const QString& evtType, + const QString& stateKey) const +{ + return contains({ evtType, stateKey }); +} + +QJsonObject RoomStateView::contentJson(const QString& evtType, + const QString& stateKey) const +{ + return queryOr(evtType, stateKey, &Event::contentJson, QJsonObject()); +} + +const QVector<const StateEvent*> RoomStateView::eventsOfType( + const QString& evtType) const +{ + auto vals = QVector<const StateEvent*>(); + for (auto it = cbegin(); it != cend(); ++it) + if (it.key().first == evtType) + vals.append(it.value()); + + return vals; +} diff --git a/lib/roomstateview.h b/lib/roomstateview.h new file mode 100644 index 00000000..c5261a1e --- /dev/null +++ b/lib/roomstateview.h @@ -0,0 +1,211 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/stateevent.h" + +#include <QtCore/QHash> + +namespace Quotient { + +class Room; + +// NB: Both concepts below expect EvT::needsStateKey to exist so you can't +// express one via negation of the other (there's still an invalid case of +// a non-state event where needsStateKey is not even defined). + +template <typename FnT, class EvT = std::decay_t<fn_arg_t<FnT>>> +concept Keyed_State_Fn = EvT::needsStateKey; + +template <typename FnT, class EvT = std::decay_t<fn_arg_t<FnT>>> +concept Keyless_State_Fn = !EvT::needsStateKey; + +class QUOTIENT_API RoomStateView + : private QHash<StateEventKey, const StateEvent*> { + Q_GADGET +public: + const QHash<StateEventKey, const StateEvent*>& events() const + { + return *this; + } + + //! \brief Get a state event with the given event type and state key + //! \return A state event corresponding to the pair of event type + //! \p evtType and state key \p stateKey, or nullptr if there's + //! no such \p evtType / \p stateKey combination in the current + //! state. + //! \warning In libQuotient 0.7 the return type changed to an OmittableCref + //! which is effectively a nullable const reference wrapper. You + //! have to check that it has_value() before using. Alternatively + //! you can now use queryCurrentState() to access state safely. + //! \sa getCurrentStateContentJson + const StateEvent* get(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief 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. It is only defined for events with state key (i.e., + //! derived from KeyedStateEvent). + template <Keyed_State_Event EvT> + const EvT* get(const QString& stateKey = {}) const + { + if (const auto* evt = get(EvT::TypeId, stateKey)) { + Q_ASSERT(evt->matrixType() == EvT::TypeId + && evt->stateKey() == stateKey); + return eventCast<const EvT>(evt); + } + return nullptr; + } + + //! \brief Get a state event with the given event type + //! + //! This is a typesafe overload that accepts a C++ event type instead of + //! its Matrix name. This overload only defined for events that do not use + //! state key (i.e., derived from KeylessStateEvent). + template <Keyless_State_Event EvT> + const EvT* get() const + { + if (const auto* evt = get(EvT::TypeId)) { + Q_ASSERT(evt->matrixType() == EvT::TypeId); + return eventCast<const EvT>(evt); + } + return nullptr; + } + + using QHash::contains; + + bool contains(const QString& evtType, const QString& stateKey = {}) const; + + template <Keyed_State_Event EvT> + bool contains(const QString& stateKey = {}) const + { + return contains(EvT::TypeId, stateKey); + } + + template <Keyless_State_Event EvT> + bool contains() const + { + return contains(EvT::TypeId); + } + + template <Keyed_State_Event EvT> + auto content(const QString& stateKey, + typename EvT::content_type defaultValue = {}) const + { + // EventBase<>::content is special in that it returns a const-ref, + // and lift() inside queryOr() can't wrap that in a temporary Omittable. + if (const auto evt = get<EvT>(stateKey)) + return evt->content(); + return std::move(defaultValue); + } + + template <Keyless_State_Event EvT> + auto content(typename EvT::content_type defaultValue = {}) const + { + // Same as above + if (const auto evt = get<EvT>()) + return evt->content(); + return defaultValue; + } + + //! \brief Get the content of the current state event with the given + //! event type and state key + //! \return An empty object if there's no event in the current state with + //! this event type and state key; the contents of the event + //! <tt>'content'</tt> object otherwise + Q_INVOKABLE QJsonObject contentJson(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief 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. + const QVector<const StateEvent*> eventsOfType(const QString& evtType) const; + + //! \brief Run a function on a state event with the given type and key + //! + //! Use this overload when there's no predefined event type or the event + //! type is unknown at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template <typename FnT> + auto query(const QString& evtType, const QString& stateKey, FnT&& fn) const + { + return lift(std::forward<FnT>(fn), get(evtType, stateKey)); + } + + //! \brief Run a function on a state event with the given type and key + //! + //! This is an overload for keyed state events (those that have + //! `needsStateKey == true`) with type defined at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template <Keyed_State_Fn FnT> + auto query(const QString& stateKey, FnT&& fn) const + { + using EventT = std::decay_t<fn_arg_t<FnT>>; + return lift(std::forward<FnT>(fn), get<EventT>(stateKey)); + } + + //! \brief Run a function on a keyless state event with the given type + //! + //! This is an overload for keyless state events (those having + //! `needsStateKey == false`) with type defined at compile time. + //! \return an Omittable with either the result of the function call, or + //! with `none` if the event is not found or the function fails + template <Keyless_State_Fn FnT> + auto query(FnT&& fn) const + { + using EventT = std::decay_t<fn_arg_t<FnT>>; + return lift(std::forward<FnT>(fn), get<EventT>()); + } + + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails + template <typename FnT, typename FallbackT> + auto queryOr(const QString& evtType, const QString& stateKey, FnT&& fn, + FallbackT&& fallback) const + { + return query(evtType, stateKey, std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails + template <typename FnT, typename FallbackT> + auto queryOr(const QString& stateKey, FnT&& fn, FallbackT&& fallback) const + { + return query(stateKey, std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + + //! \brief Same as query() but with a fallback value + //! + //! This is a shortcut for `query().value_or()`, passing respective + //! arguments to the respective functions. This is an overload for the case + //! when the event type cannot be fixed at compile time. + //! \return the result of \p fn execution, or \p fallback if the requested + //! event doesn't exist or the function fails + template <typename FnT, typename FallbackT> + auto queryOr(FnT&& fn, FallbackT&& fallback) const + { + return query(std::forward<FnT>(fn)) + .value_or(std::forward<FallbackT>(fallback)); + } + +private: + friend class Room; +}; +} // namespace Quotient diff --git a/lib/settings.cpp b/lib/settings.cpp index dd086d9c..510d253c 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -1,5 +1,9 @@ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "settings.h" +#include "util.h" #include "logging.h" #include <QtCore/QUrl> @@ -18,7 +22,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) @@ -97,17 +103,18 @@ void SettingsGroup::remove(const QString& key) Settings::remove(fullKey); } -QTNT_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, +QUO_DEFINE_SETTING(AccountSettings, QString, deviceId, "device_id", {}, setDeviceId) -QTNT_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, +QUO_DEFINE_SETTING(AccountSettings, QString, deviceName, "device_name", {}, setDeviceName) -QTNT_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, +QUO_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, setKeepLoggedIn) -static const auto HomeserverKey = QStringLiteral("homeserver"); -static const auto AccessTokenKey = QStringLiteral("access_token"); -static const auto EncryptionAccountPickleKey = - QStringLiteral("encryption_account_pickle"); +namespace { +constexpr auto HomeserverKey = "homeserver"_ls; +constexpr auto AccessTokenKey = "access_token"_ls; +constexpr auto EncryptionAccountPickleKey = "encryption_account_pickle"_ls; +} QUrl AccountSettings::homeserver() const { @@ -121,19 +128,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); @@ -144,18 +138,12 @@ void AccountSettings::clearAccessToken() QByteArray AccountSettings::encryptionAccountPickle() { - QString passphrase = ""; // FIXME: add QtKeychain return value("encryption_account_pickle", "").toByteArray(); } void AccountSettings::setEncryptionAccountPickle( const QByteArray& encryptionAccountPickle) { - qCWarning(MAIN) - << "Saving encryption_account_pickle to QSettings is insecure." - " Developers, do it manually or contribute to share QtKeychain " - "logic to libQuotient."; - QString passphrase = ""; // FIXME: add QtKeychain setValue("encryption_account_pickle", encryptionAccountPickle); } diff --git a/lib/settings.h b/lib/settings.h index c45764a6..ff99d488 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -1,23 +1,10 @@ -/****************************************************************************** - * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "quotient_export.h" + #include <QtCore/QSettings> #include <QtCore/QUrl> #include <QtCore/QVector> @@ -26,7 +13,7 @@ class QVariant; namespace Quotient { -class Settings : public QSettings { +class QUOTIENT_API Settings : public QSettings { Q_OBJECT public: /// Add a legacy organisation/application name to migrate settings from @@ -91,11 +78,10 @@ protected: QSettings legacySettings { legacyOrganizationName, legacyApplicationName }; }; -class SettingsGroup : public Settings { +class QUOTIENT_API 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)) {} @@ -120,7 +106,7 @@ private: QString groupPath; }; -#define QTNT_DECLARE_SETTING(type, propname, setter) \ +#define QUO_DECLARE_SETTING(type, propname, setter) \ Q_PROPERTY(type propname READ propname WRITE setter) \ public: \ type propname() const; \ @@ -128,7 +114,7 @@ public: \ \ private: -#define QTNT_DEFINE_SETTING(classname, type, propname, qsettingname, \ +#define QUO_DEFINE_SETTING(classname, type, propname, qsettingname, \ defaultValue, setter) \ type classname::propname() const \ { \ @@ -140,21 +126,17 @@ private: setValue(QStringLiteral(qsettingname), std::move(newValue)); \ } -class AccountSettings : public SettingsGroup { +class QUOTIENT_API AccountSettings : public SettingsGroup { Q_OBJECT Q_PROPERTY(QString userId READ userId CONSTANT) - QTNT_DECLARE_SETTING(QString, deviceId, setDeviceId) - QTNT_DECLARE_SETTING(QString, deviceName, setDeviceName) - QTNT_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) - /** \deprecated \sa setAccessToken */ - Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) + QUO_DECLARE_SETTING(QString, deviceId, setDeviceId) + QUO_DECLARE_SETTING(QString, deviceName, setDeviceName) + QUO_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) 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; @@ -162,11 +144,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/ssosession.cpp b/lib/ssosession.cpp index be701204..93e252cc 100644 --- a/lib/ssosession.cpp +++ b/lib/ssosession.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "ssosession.h" #include "connection.h" @@ -12,10 +15,10 @@ using namespace Quotient; class SsoSession::Private { public: - Private(SsoSession* q, const QString& initialDeviceName = {}, - const QString& deviceId = {}, Connection* connection = nullptr) - : initialDeviceName(initialDeviceName) - , deviceId(deviceId) + Private(SsoSession* q, QString initialDeviceName = {}, + QString deviceId = {}, Connection* connection = nullptr) + : initialDeviceName(std::move(initialDeviceName)) + , deviceId(std::move(deviceId)) , connection(connection) { auto* server = new QTcpServer(q); @@ -26,7 +29,7 @@ public: .arg(server->serverPort()); ssoUrl = connection->getUrlForApi<RedirectToSSOJob>(callbackUrl); - QObject::connect(server, &QTcpServer::newConnection, q, [this, server] { + QObject::connect(server, &QTcpServer::newConnection, q, [this, q, server] { qCDebug(MAIN) << "SSO callback initiated"; socket = server->nextPendingConnection(); server->close(); @@ -40,8 +43,14 @@ public: }); QObject::connect(socket, &QTcpSocket::disconnected, socket, &QTcpSocket::deleteLater); + QObject::connect(socket, &QObject::destroyed, q, + &QObject::deleteLater); }); + qCDebug(MAIN) << "SSO session constructed"; } + ~Private() { qCDebug(MAIN) << "SSO session deconstructed"; } + Q_DISABLE_COPY_MOVE(Private) + void processCallback(); void sendHttpResponse(const QByteArray& code, const QByteArray& msg); void onError(const QByteArray& code, const QString& errorMsg); @@ -58,19 +67,12 @@ public: SsoSession::SsoSession(Connection* connection, const QString& initialDeviceName, const QString& deviceId) : QObject(connection) - , d(std::make_unique<Private>(this, initialDeviceName, deviceId, connection)) -{ - qCDebug(MAIN) << "SSO session constructed"; -} - -SsoSession::~SsoSession() -{ - qCDebug(MAIN) << "SSO session deconstructed"; -} + , d(makeImpl<Private>(this, initialDeviceName, deviceId, connection)) +{} QUrl SsoSession::ssoUrl() const { return d->ssoUrl; } -QUrl SsoSession::callbackUrl() const { return d->callbackUrl; } +QUrl SsoSession::callbackUrl() const { return QUrl(d->callbackUrl); } void SsoSession::Private::processCallback() { @@ -79,29 +81,29 @@ void SsoSession::Private::processCallback() // (see at https://github.com/clementine-player/Clementine/) const auto& requestParts = requestData.split(' '); if (requestParts.size() < 2 || requestParts[1].isEmpty()) { - onError("400 Bad Request", tr("No login token in SSO callback")); + onError("400 Bad Request", tr("Malformed single sign-on callback")); return; } const auto& QueryItemName = QStringLiteral("loginToken"); QUrlQuery query { QUrl(requestParts[1]).query() }; if (!query.hasQueryItem(QueryItemName)) { - onError("400 Bad Request", tr("Malformed single sign-on callback")); + onError("400 Bad Request", tr("No login token in SSO callback")); + return; } qCDebug(MAIN) << "Found the token in SSO callback, logging in"; connection->loginWithToken(query.queryItemValue(QueryItemName).toLatin1(), initialDeviceName, deviceId); connect(connection, &Connection::connected, socket, [this] { - const QString msg = - "The application '" % QCoreApplication::applicationName() - % "' has successfully logged in as a user " % connection->userId() - % " with device id " % connection->deviceId() - % ". This window can be closed. Thank you.\r\n"; + const auto msg = + tr("The application '%1' has successfully logged in as a user %2 " + "with device id %3. This window can be closed. Thank you.\r\n") + .arg(QCoreApplication::applicationName(), connection->userId(), + connection->deviceId()); sendHttpResponse("200 OK", msg.toHtmlEscaped().toUtf8()); socket->disconnectFromHost(); }); connect(connection, &Connection::loginError, socket, [this] { onError("401 Unauthorised", tr("Login failed")); - socket->disconnectFromHost(); }); } @@ -125,4 +127,5 @@ void SsoSession::Private::onError(const QByteArray& code, // [kitsune] Yeah, I know, dirty. Maybe the "right" way would be to have // an intermediate signal but that seems just a fight for purity. emit connection->loginError(errorMsg, requestData); + socket->disconnectFromHost(); } diff --git a/lib/ssosession.h b/lib/ssosession.h index 5845cd4d..e6a3f8fb 100644 --- a/lib/ssosession.h +++ b/lib/ssosession.h @@ -1,13 +1,13 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once +#include "util.h" + #include <QtCore/QUrl> #include <QtCore/QObject> -#include <memory> - -class QTcpServer; -class QTcpSocket; - namespace Quotient { class Connection; @@ -26,19 +26,20 @@ class Connection; * connection->prepareForSso(initialDeviceName)->ssoUrl()); * \endcode */ -class SsoSession : public QObject { +class QUOTIENT_API SsoSession : public QObject { Q_OBJECT Q_PROPERTY(QUrl ssoUrl READ ssoUrl CONSTANT) Q_PROPERTY(QUrl callbackUrl READ callbackUrl CONSTANT) public: SsoSession(Connection* connection, const QString& initialDeviceName, const QString& deviceId = {}); - ~SsoSession() override; + ~SsoSession() override = default; + QUrl ssoUrl() const; QUrl callbackUrl() const; private: class Private; - std::unique_ptr<Private> d; + ImplPtr<Private> d; }; } // namespace Quotient diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index a3809469..ec7203af 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -1,33 +1,15 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "syncdata.h" -#include "events/eventloader.h" +#include "logging.h" #include <QtCore/QFile> #include <QtCore/QFileInfo> using namespace Quotient; -const QString SyncRoomData::UnreadCountKey = - QStringLiteral("x-quotient.unread_count"); - bool RoomSummary::isEmpty() const { return !joinedMemberCount && !invitedMemberCount && !heroes; @@ -36,9 +18,10 @@ bool RoomSummary::isEmpty() const bool RoomSummary::merge(const RoomSummary& other) { // Using bitwise OR to prevent computation shortcut. - return joinedMemberCount.merge(other.joinedMemberCount) - | invitedMemberCount.merge(other.invitedMemberCount) - | heroes.merge(other.heroes); + return static_cast<bool>( + static_cast<int>(joinedMemberCount.merge(other.joinedMemberCount)) + | static_cast<int>(invitedMemberCount.merge(other.invitedMemberCount)) + | static_cast<int>(heroes.merge(other.heroes))); } QDebug Quotient::operator<<(QDebug dbg, const RoomSummary& rs) @@ -79,23 +62,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(); @@ -104,17 +87,52 @@ 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(-1); - notificationCount = unreadJson.value("notification_count"_ls).toInt(-1); + 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); +} + +QDebug Quotient::operator<<(QDebug dbg, const DevicesList& devicesList) +{ + QDebugStateSaver _(dbg); + QStringList sl; + if (!devicesList.changed.isEmpty()) + sl << QStringLiteral("changed: %1").arg(devicesList.changed.join(", ")); + if (!devicesList.left.isEmpty()) + sl << QStringLiteral("left %1").arg(devicesList.left.join(", ")); + dbg.nospace().noquote() << sl.join(QStringLiteral("; ")); + return dbg; +} + +void JsonObjectConverter<DevicesList>::dumpTo(QJsonObject& jo, + const DevicesList& rs) +{ + addParam<IfNotEmpty>(jo, QStringLiteral("changed"), + rs.changed); + addParam<IfNotEmpty>(jo, QStringLiteral("left"), + rs.left); +} + +void JsonObjectConverter<DevicesList>::fillFrom(const QJsonObject& jo, + DevicesList& rs) +{ + fromJson(jo["changed"_ls], rs.changed); + fromJson(jo["left"_ls], rs.left); } 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) @@ -125,7 +143,7 @@ SyncData::SyncData(const QString& cacheFileName) << "is required; discarding the cache"; } -SyncDataList&& SyncData::takeRoomData() { return move(roomData); } +SyncDataList SyncData::takeRoomData() { return move(roomData); } QString SyncData::fileNameForRoom(QString roomId) { @@ -133,11 +151,18 @@ QString SyncData::fileNameForRoom(QString roomId) return roomId + ".json"; } -Events&& SyncData::takePresenceData() { return std::move(presenceData); } +Events SyncData::takePresenceData() { return std::move(presenceData); } -Events&& SyncData::takeAccountData() { return std::move(accountData); } +Events SyncData::takeAccountData() { return std::move(accountData); } -Events&& SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } +Events SyncData::takeToDeviceEvents() { return std::move(toDeviceEvents); } + +std::pair<int, int> SyncData::cacheVersion() +{ + return { MajorCacheVersion, 2 }; +} + +DevicesList SyncData::takeDevicesList() { return std::move(devicesList); } QJsonObject SyncData::loadJson(const QString& fileName) { @@ -155,12 +180,7 @@ QJsonObject SyncData::loadJson(const QString& fileName) const auto json = data.startsWith('{') ? QJsonDocument::fromJson(data).object() -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) - : QCborValue::fromCbor(data).toJsonValue().toObject() -#else - : QJsonDocument::fromBinaryData(data).object() -#endif - ; + : QCborValue::fromCbor(data).toJsonValue().toObject(); if (json.isEmpty()) { qCWarning(MAIN) << "State cache in" << fileName << "is broken or empty, discarding"; @@ -181,24 +201,32 @@ void SyncData::parseJson(const QJsonObject& json, const QString& baseDir) fromJson(json.value("device_one_time_keys_count"_ls), deviceOneTimeKeysCount_); + if(json.contains("device_lists")) { + fromJson(json.value("device_lists"), devicesList); + } + auto rooms = json.value("rooms"_ls).toObject(); auto totalRooms = 0; auto totalEvents = 0; for (size_t i = 0; i < JoinStateStrings.size(); ++i) { - // This assumes that JoinState values go over powers of 2: 1,2,4,... + // 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(roomData.size() + static_cast<size_t>(rs.size())); for (auto roomIt = rs.begin(); roomIt != rs.end(); ++roomIt) { - auto roomJson = - roomIt->isObject() - ? roomIt->toObject() - : loadJson(baseDir + fileNameForRoom(roomIt.key())); - if (roomJson.isEmpty()) { - unresolvedRoomIds.push_back(roomIt.key()); - continue; - } + QJsonObject roomJson; + if (!baseDir.isEmpty()) { + // Loading data from the local cache, with room objects saved in + // individual files rather than inline + roomJson = loadJson(baseDir + fileNameForRoom(roomIt.key())); + if (roomJson.isEmpty()) { + unresolvedRoomIds.push_back(roomIt.key()); + continue; + } + } else // When loading from /sync response, everything is inline + roomJson = roomIt->toObject(); + roomData.emplace_back(roomIt.key(), joinState, roomJson); const auto& r = roomData.back(); totalEvents += r.state.size() + r.ephemeral.size() diff --git a/lib/syncdata.h b/lib/syncdata.h index 67d04557..9358ec8f 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -1,28 +1,19 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #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 @@ -31,7 +22,7 @@ namespace Quotient { * means that nothing has come from the server; heroes.value().isEmpty() * means a peculiar case of a room with the only member - the current user. */ -struct RoomSummary { +struct QUOTIENT_API RoomSummary { Omittable<int> joinedMemberCount; Omittable<int> invitedMemberCount; Omittable<QStringList> heroes; //< mxids of users to take part in the room @@ -44,13 +35,33 @@ struct RoomSummary { }; QDebug operator<<(QDebug dbg, const RoomSummary& rs); - template <> struct JsonObjectConverter<RoomSummary> { static void dumpTo(QJsonObject& jo, const RoomSummary& rs); static void fillFrom(const QJsonObject& jo, RoomSummary& rs); }; +/// Information on e2e device updates. Note: only present on an +/// incremental sync. +struct DevicesList { + /// List of users who have updated their device identity keys, or who + /// now share an encrypted room with the client since the previous + /// sync response. + QStringList changed; + + /// List of users with whom we do not share any encrypted rooms + /// anymore since the previous sync response. + QStringList left; +}; + +QDebug operator<<(QDebug dhg, const DevicesList& devicesList); + +template <> +struct JsonObjectConverter<DevicesList> { + static void dumpTo(QJsonObject &jo, const DevicesList &dev); + static void fillFrom(const QJsonObject& jo, DevicesList& rs); +}; + class SyncRoomData { public: QString roomId; @@ -63,16 +74,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. @@ -89,20 +98,22 @@ public: */ void parseJson(const QJsonObject& json, const QString& baseDir = {}); - Events&& takePresenceData(); - Events&& takeAccountData(); - Events&& takeToDeviceEvents(); + Events takePresenceData(); + Events takeAccountData(); + Events takeToDeviceEvents(); const QHash<QString, int>& deviceOneTimeKeysCount() const { return deviceOneTimeKeysCount_; } - SyncDataList&& takeRoomData(); + SyncDataList takeRoomData(); + DevicesList takeDevicesList(); QString nextBatch() const { return nextBatch_; } 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: @@ -113,6 +124,7 @@ private: SyncDataList roomData; QStringList unresolvedRoomIds; QHash<QString, int> deviceOneTimeKeysCount_; + DevicesList devicesList; static QJsonObject loadJson(const QString& fileName); }; diff --git a/lib/uri.cpp b/lib/uri.cpp index 9eefdc83..91751df0 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -1,28 +1,36 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "uri.h" +#include "util.h" #include "logging.h" #include <QtCore/QRegularExpression> using namespace Quotient; -struct ReplacePair { QByteArray uriString; char sigil; }; +namespace { + +struct ReplacePair { QLatin1String uriString; char sigil; }; /// \brief Defines bi-directional mapping of path prefixes and sigils /// /// When there are two prefixes for the same sigil, the first matching /// entry for a given sigil is used. -static const auto replacePairs = { - ReplacePair { "u/", '@' }, - { "user/", '@' }, - { "roomid/", '!' }, - { "r/", '#' }, - { "room/", '#' }, +const ReplacePair replacePairs[] = { + { "u/"_ls, '@' }, + { "user/"_ls, '@' }, + { "roomid/"_ls, '!' }, + { "r/"_ls, '#' }, + { "room/"_ls, '#' }, // The notation for bare event ids is not proposed in MSC2312 but there's // https://github.com/matrix-org/matrix-doc/pull/2644 - { "e/", '$' }, - { "event/", '$' } + { "e/"_ls, '$' }, + { "event/"_ls, '$' } }; +} + Uri::Uri(QByteArray primaryId, QByteArray secondaryId, QString query) { if (primaryId.isEmpty()) @@ -67,12 +75,12 @@ 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(); } -static auto matrixToUrlRegexInit() +static inline auto matrixToUrlRegexInit() { // See https://matrix.org/docs/spec/appendices#matrix-to-navigation const QRegularExpression MatrixToUrlRE { @@ -95,7 +103,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; @@ -125,9 +133,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")) }; } } @@ -163,7 +171,7 @@ QUrl Uri::toUrl(UriForm form) const return {}; if (form == CanonicalUri || type() == NonMatrix) - return *this; // NOLINT(cppcoreguidelines-slicing): It's intentional + return SLICE(*this, QUrl); QUrl url; url.setScheme("https"); @@ -183,14 +191,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"); @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "quotient_common.h" @@ -20,7 +23,7 @@ namespace Quotient { * its type, and obtain components, also in either unencoded (for displaying) * or encoded (for APIs) form. */ -class Uri : private QUrl { +class QUOTIENT_API Uri : private QUrl { Q_GADGET public: enum Type : char { diff --git a/lib/uriresolver.cpp b/lib/uriresolver.cpp index e5f19a96..681e3842 100644 --- a/lib/uriresolver.cpp +++ b/lib/uriresolver.cpp @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #include "uriresolver.h" #include "connection.h" @@ -5,6 +8,8 @@ using namespace Quotient; +UriResolverBase::~UriResolverBase() = default; + UriResolveResult UriResolverBase::visitResource(Connection* account, const Uri& uri) { @@ -24,9 +29,9 @@ UriResolveResult UriResolverBase::visitResource(Connection* account, case Uri::UserId: { if (uri.action() == "join") return IncorrectAction; - if (auto* const user = account->user(uri.primaryId())) - return visitUser(user, uri.action()); - return InvalidUri; + auto* user = account->user(uri.primaryId()); + Q_ASSERT(user != nullptr); + return visitUser(user, uri.action()); } case Uri::RoomId: case Uri::RoomAlias: { diff --git a/lib/uriresolver.h b/lib/uriresolver.h index 9b2ced9d..9140046c 100644 --- a/lib/uriresolver.h +++ b/lib/uriresolver.h @@ -1,3 +1,6 @@ +// SPDX-FileCopyrightText: 2020 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later + #pragma once #include "uri.h" @@ -22,7 +25,7 @@ class User; * gradual implementation. Derived classes are encouraged to override as many * of them as possible. */ -class UriResolverBase { +class QUOTIENT_API UriResolverBase { public: /*! \brief Resolve the resource and dispatch an action depending on its type * @@ -39,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` /*! @@ -64,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 @@ -93,7 +105,7 @@ protected: * * \sa UriResolverBase, UriDispatcher */ -UriResolveResult +QUOTIENT_API UriResolveResult visitResource(Connection* account, const Uri& uri, std::function<UriResolveResult(User*, QString)> userHandler, std::function<void(Room*, QString)> roomEventHandler, @@ -129,7 +141,7 @@ inline UriResolveResult checkResource(Connection* account, const Uri& uri) * synchronously - the returned value is the result of resolving the URI, * not acting on it. */ -class UriDispatcher : public QObject, public UriResolverBase { +class QUOTIENT_API UriDispatcher : public QObject, public UriResolverBase { Q_OBJECT public: explicit UriDispatcher(QObject* parent = nullptr) : QObject(parent) {} @@ -141,7 +153,7 @@ public: return UriResolverBase::visitResource(account, uri); } -signals: +Q_SIGNALS: /// An action on a user has been requested void userAction(Quotient::User* user, QString action); diff --git a/lib/user.cpp b/lib/user.cpp index 4e369a4f..4c3fc9e2 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -1,20 +1,6 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "user.h" @@ -47,47 +33,26 @@ public: QString id; qreal hueF; - // In the following two, isNull/nullopt mean they are uninitialised; - // isEmpty/Avatar::url().isEmpty() mean they are initialised but empty. QString defaultName; - std::optional<Avatar> defaultAvatar; - + Avatar defaultAvatar; // NB: This container is ever-growing. Even if the user no more scrolls // the timeline that far back, historical avatars are still kept around. // This is consistent with the rest of Quotient, as room timelines - // are never rotated either. This will probably change in the future. + // are never vacuumed either. This will probably change in the future. /// Map of mediaId to Avatar objects static UnorderedMap<QString, Avatar> otherAvatars; - - void fetchProfile(const User* q); - - template <typename SourceT> - bool doSetAvatar(SourceT&& source, User* q); }; decltype(User::Private::otherAvatars) User::Private::otherAvatars {}; -void User::Private::fetchProfile(const User* q) -{ - defaultAvatar.emplace(Avatar {}); - defaultName = ""; - auto* j = - q->connection()->callApi<GetUserProfileJob>(BackgroundRequest, - QUrl::toPercentEncoding(id)); - // FIXME: accepting const User* and const_cast'ing it here is only - // until we get a better User API in 0.7 - QObject::connect(j, &BaseJob::success, q, - [this, q = const_cast<User*>(q), j] { - q->updateName(j->displayname()); - defaultAvatar->updateUrl(j->avatarUrl()); - emit q->avatarChanged(q, nullptr); - }); -} - User::User(QString userId, Connection* connection) - : QObject(connection), d(new Private(move(userId))) + : QObject(connection), d(makeImpl<Private>(move(userId))) { setObjectName(id()); + if (connection->userId() == id()) { + // Load profile information for local user. + load(); + } } Connection* User::connection() const @@ -96,7 +61,17 @@ Connection* User::connection() const return static_cast<Connection*>(parent()); } -User::~User() = default; +void User::load() +{ + auto* profileJob = + connection()->callApi<GetUserProfileJob>(id()); + connect(profileJob, &BaseJob::result, this, [this, profileJob] { + d->defaultName = profileJob->displayname(); + d->defaultAvatar = Avatar(QUrl(profileJob->avatarUrl())); + emit defaultNameChanged(); + emit defaultAvatarChanged(); + }); +} QString User::id() const { return d->id; } @@ -105,52 +80,16 @@ 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 == ':'; } int User::hue() const { return int(hueF() * 359); } -/// \sa https://github.com/matrix-org/matrix-doc/issues/1375 -/// -/// Relies on untrusted prevContent so can't be put to RoomMemberEvent and -/// in general should rather be remade in terms of the room's eventual "state -/// time machine" -QString getBestKnownName(const RoomMemberEvent* event) -{ - const auto& jv = event->contentJson().value("displayname"_ls); - return !jv.isUndefined() - ? jv.toString() - : event->prevContent() ? event->prevContent()->displayName - : QString(); -} - QString User::name(const Room* room) const { - if (room) - return getBestKnownName(room->getCurrentState<RoomMemberEvent>(id())); - - if (d->defaultName.isNull()) - d->fetchProfile(this); - - return d->defaultName; -} - -QString User::rawName(const Room* room) const { return name(room); } - -void User::updateName(const QString& newName, const Room* r) -{ - Q_ASSERT(r == nullptr); - if (newName == d->defaultName) - return; - - emit nameAboutToChange(newName, d->defaultName, nullptr); - const auto& oldName = - std::exchange(d->defaultName, newName); - emit nameChanged(d->defaultName, oldName, nullptr); + return room ? room->memberName(id()) : d->defaultName; } -void User::updateName(const QString&, const QString&, const Room*) {} -void User::updateAvatarUrl(const QUrl&, const QUrl&, const Room*) {} void User::rename(const QString& newName) { @@ -160,12 +99,18 @@ void User::rename(const QString& newName) connect(connection()->callApi<SetDisplayNameJob>(id(), actualNewName), &BaseJob::success, this, [this, actualNewName] { - d->fetchProfile(this); - updateName(actualNewName); + // Check again, it could have changed meanwhile + if (actualNewName != d->defaultName) { + d->defaultName = actualNewName; + emit defaultNameChanged(); + } else + qCWarning(MAIN) + << "User" << id() << "already has profile name set to" + << actualNewName; }); } -void User::rename(const QString& newName, const Room* r) +void User::rename(const QString& newName, Room* r) { if (!r) { qCWarning(MAIN) << "Passing a null room to two-argument User::rename()" @@ -174,51 +119,51 @@ void User::rename(const QString& newName, const Room* r) return; } // #481: take the current state and update it with the new name - auto evtC = r->getCurrentState<RoomMemberEvent>(id())->content(); - Q_ASSERT_X(evtC.membership == MembershipType::Join, __FUNCTION__, - "Attempt to rename a user that's not a room member"); - evtC.displayName = sanitized(newName); - r->setState<RoomMemberEvent>(id(), move(evtC)); - // The state will be updated locally after it arrives with sync + if (const auto& maybeEvt = r->currentState().get<RoomMemberEvent>(id())) { + auto content = maybeEvt->content(); + if (content.membership == Membership::Join) { + content.displayName = sanitized(newName); + r->setState<RoomMemberEvent>(id(), move(content)); + // The state will be updated locally after it arrives with sync + return; + } + } + qCCritical(MEMBERS) + << "Attempt to rename a non-member in a room context - ignored"; } template <typename SourceT> -bool User::Private::doSetAvatar(SourceT&& source, User* q) -{ - if (!defaultAvatar) { - defaultName = ""; - defaultAvatar.emplace(Avatar {}); - } - return defaultAvatar->upload( - q->connection(), source, [this, q](const QString& contentUri) { - auto* j = - q->connection()->callApi<SetAvatarUrlJob>(id, contentUri); - QObject::connect(j, &BaseJob::success, q, - [this, q, newUrl = QUrl(contentUri)] { - // Fetch displayname to complete the profile - fetchProfile(q); - if (newUrl == defaultAvatar->url()) { - qCWarning(MAIN) - << "User" << id - << "already has avatar URL set to" - << newUrl.toDisplayString(); - return; - } - - defaultAvatar->updateUrl(newUrl); - emit q->avatarChanged(q, nullptr); - }); +inline bool User::doSetAvatar(SourceT&& source) +{ + return d->defaultAvatar.upload( + connection(), source, [this](const QUrl& contentUri) { + auto* j = connection()->callApi<SetAvatarUrlJob>(id(), contentUri); + connect(j, &BaseJob::success, this, + [this, contentUri] { + if (contentUri == d->defaultAvatar.url()) { + d->defaultAvatar.updateUrl(contentUri); + emit defaultAvatarChanged(); + } else + qCWarning(MAIN) << "User" << id() + << "already has avatar URL set to" + << contentUri.toDisplayString(); + }); }); } bool User::setAvatar(const QString& fileName) { - return d->doSetAvatar(fileName, this); + return doSetAvatar(fileName); } bool User::setAvatar(QIODevice* source) { - return d->doSetAvatar(source, this); + return doSetAvatar(source); +} + +void User::removeAvatar() +{ + connection()->callApi<SetAvatarUrlJob>(id(), QUrl()); } void User::requestDirectChat() { connection()->requestDirectChat(this); } @@ -231,13 +176,8 @@ bool User::isIgnored() const { return connection()->isIgnored(this); } QString User::displayname(const Room* room) const { - if (room) - return room->roomMembername(this); - - if (auto n = name(); !n.isEmpty()) - return n; - - return d->id; + return room ? room->safeMemberName(id()) + : d->defaultName.isEmpty() ? d->id : d->defaultName; } QString User::fullName(const Room* room) const @@ -246,50 +186,30 @@ QString User::fullName(const Room* room) const return displayName.isEmpty() ? id() : (displayName % " (" % id() % ')'); } -QString User::bridged() const { return {}; } - -/// \sa getBestKnownName, https://github.com/matrix-org/matrix-doc/issues/1375 -QUrl getBestKnownAvatarUrl(const RoomMemberEvent* event) -{ - const auto& jv = event->contentJson().value("avatar_url"_ls); - return !jv.isUndefined() - ? jv.toString() - : event->prevContent() ? event->prevContent()->avatarUrl - : QUrl(); -} - const Avatar& User::avatarObject(const Room* room) const { - if (!room) { - if (!d->defaultAvatar) { - d->fetchProfile(this); - } - return *d->defaultAvatar; - } + if (!room) + return d->defaultAvatar; - const auto& url = - getBestKnownAvatarUrl(room->getCurrentState<RoomMemberEvent>(id())); + const auto& url = room->memberAvatarUrl(id()); const auto& mediaId = url.authority() + url.path(); return d->otherAvatars.try_emplace(mediaId, url).first->second; } -QImage User::avatar(int dimension, const Room* room) +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, [=] { - emit avatarChanged(this, room); - callback(); - }); + return avatarObject(room).get(connection(), width, height, callback); } QString User::avatarMediaId(const Room* room) const @@ -302,32 +222,4 @@ QUrl User::avatarUrl(const Room* room) const return avatarObject(room).url(); } -void User::processEvent(const RoomMemberEvent& event, const Room* room, - bool firstMention) -{ - Q_ASSERT(room); - - // This is prone to abuse if prevContent is forged; only here until 0.7 - // (and the whole method, actually). - const auto& oldName = event.prevContent() ? event.prevContent()->displayName - : QString(); - const auto& newName = getBestKnownName(&event); - // A hacky way to find out if it's about to change or already changed; - // making it a lambda allows to omit stub event creation when unneeded - const auto& isAboutToChange = [&event, room, this] { - return room->getCurrentState<RoomMemberEvent>(id()) != &event; - }; - if (firstMention || newName != oldName) { - if (isAboutToChange()) - emit nameAboutToChange(newName, oldName, room); - else - emit nameChanged(newName, oldName, room); - } - const auto& oldAvatarUrl = - event.prevContent() ? event.prevContent()->avatarUrl : QUrl(); - const auto& newAvatarUrl = getBestKnownAvatarUrl(&event); - if ((firstMention || newAvatarUrl != oldAvatarUrl) && !isAboutToChange()) - emit avatarChanged(this, room); -} - qreal User::hueF() const { return d->hueF; } @@ -1,24 +1,11 @@ -/****************************************************************************** - * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2015 Felix Rohrbach <kde@fxrh.de> +// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "avatar.h" +#include "util.h" #include <QtCore/QObject> @@ -27,22 +14,19 @@ class Connection; class Room; class RoomMemberEvent; -class User : public QObject { +class QUOTIENT_API User : public QObject { Q_OBJECT Q_PROPERTY(QString id READ id CONSTANT) Q_PROPERTY(bool isGuest READ isGuest CONSTANT) Q_PROPERTY(int hue READ hue CONSTANT) Q_PROPERTY(qreal hueF READ hueF CONSTANT) - Q_PROPERTY(QString name READ name NOTIFY nameChanged) - Q_PROPERTY(QString displayName READ displayname NOTIFY nameChanged STORED false) - Q_PROPERTY(QString fullName READ fullName NOTIFY nameChanged STORED false) - Q_PROPERTY(QString bridgeName READ bridged NOTIFY nameChanged STORED false) - Q_PROPERTY(QString avatarMediaId READ avatarMediaId NOTIFY avatarChanged - STORED false) - Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY avatarChanged) + 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 avatarMediaId READ avatarMediaId NOTIFY defaultAvatarChanged STORED false) + Q_PROPERTY(QUrl avatarUrl READ avatarUrl NOTIFY defaultAvatarChanged) public: User(QString userId, Connection* connection); - ~User() override; Connection* connection() const; @@ -55,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. @@ -85,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 @@ -114,31 +83,26 @@ 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; - // TODO: This method is only there to emit obsolete signals: - // nameAboutToChange(), nameChanged() and avatarChanged() - all of these - // to be removed in 0.7 - /// \deprecated - void processEvent(const RoomMemberEvent& event, const Room* r, - bool firstMention); - -public slots: +public Q_SLOTS: /// Set a new name in the global user profile void rename(const QString& newName); /// Set a new name for the user in one room - void rename(const QString& newName, const Room* r); + void rename(const QString& newName, Room* r); /// Upload the file and use it as an avatar bool setAvatar(const QString& fileName); /// Upload contents of the QIODevice and set that as an avatar bool setAvatar(QIODevice* source); + /// Removes the avatar from the profile + void removeAvatar(); /// Create or find a direct chat with this user /*! The resulting chat is returned asynchronously via * Connection::directChatAvailable() @@ -150,27 +114,20 @@ public slots: void unmarkIgnore(); /// Check whether the user is in ignore list bool isIgnored() const; + /// Force loading displayName and avartar url. This is required in + /// some cases where the you need to use an user independent of the + /// room. + void load(); -signals: - /// \deprecated Use Room::memberListChanged() for member changes - void nameAboutToChange(QString newName, QString oldName, - const Quotient::Room* roomContext); - /// \deprecated Use Room::memberListChanged() for member changes - void nameChanged(QString newName, QString oldName, - const Quotient::Room* roomContext); - /// \deprecated Use Room::memberListChanged() for member changes - void avatarChanged(Quotient::User* user, const Quotient::Room* roomContext); - -private slots: // TODO: remove in 0.7 - /// \deprecated - void updateName(const QString& newName, const Room* r = nullptr); - /// \deprecated - void updateName(const QString&, const QString&, const Room* = nullptr); - /// \deprecated - void updateAvatarUrl(const QUrl&, const QUrl&, const Room* = nullptr); +Q_SIGNALS: + void defaultNameChanged(); + void defaultAvatarChanged(); private: class Private; - QScopedPointer<Private> d; + ImplPtr<Private> d; + + template <typename SourceT> + bool doSetAvatar(SourceT&& source); }; } // namespace Quotient diff --git a/lib/util.cpp b/lib/util.cpp index cf5e81a3..359b2959 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -1,20 +1,6 @@ -/****************************************************************************** - * Copyright (C) 2018 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2018 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #include "util.h" @@ -28,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 @@ -118,7 +101,7 @@ qreal Quotient::stringToHueF(const QString& s) } static const auto ServerPartRegEx = QStringLiteral( - "(\\[[^][:blank:]]+\\]|[-[:alnum:].]+)" // Either IPv6 address or hostname/IPv4 address + "(\\[[^][:space:]]+]|[-[:alnum:].]+)" // IPv6 address or hostname/IPv4 address "(?::(\\d{1,5}))?" // Optional port ); @@ -133,34 +116,31 @@ 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; +} + +bool Quotient::encryptionSupported() +{ +#ifdef Quotient_E2EE_ENABLED + return true; +#else + return false; +#endif +} @@ -1,35 +1,66 @@ -/****************************************************************************** - * Copyright (C) 2016 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2016 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "quotient_export.h" + #include <QtCore/QLatin1String> #include <QtCore/QHashFunctions> -#include <functional> #include <memory> #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 + +#if __cpp_conditional_explicit >= 201806L +#define QUO_IMPLICIT explicit(false) +#else +#define QUO_IMPLICIT +#endif + +#define DECL_DEPRECATED_ENUMERATOR(Deprecated, Recommended) \ + Deprecated Q_DECL_ENUMERATOR_DEPRECATED_X("Use " #Recommended) = Recommended + +/// \brief Copy an object with slicing +/// +/// Unintended slicing is bad, which why there's a C++ Core Guideline that +/// basically says "don't slice, or if you do, make it explicit". Sonar and +/// clang-tidy have warnings matching this guideline; unfortunately, those +/// warnings trigger even when you have a dedicated method (as the guideline +/// recommends) that makes a slicing copy. +/// +/// This macro is meant for cases when slicing is intended: the static cast +/// silences the static analysis warning, and the macro appearance itself makes +/// it very clear that slicing is wanted here. It is made as a macro +/// (not as a function template) to support the case of private inheritance +/// in which a function template would not be able to cast to the private base +/// (see Uri::toUrl() for an example of just that situation). +#define SLICE(Object, ToType) ToType{static_cast<const ToType&>(Object)} namespace Quotient { /// An equivalent of std::hash for QTypes to enable std::unordered_map<QType, ...> @@ -44,173 +75,7 @@ struct HashQ { template <typename KeyT, typename ValT> using UnorderedMap = std::unordered_map<KeyT, ValT, HashQ<KeyT>>; -constexpr auto none = std::nullopt; - -/** `std::optional` with tweaks - * - * The tweaks are: - * - streamlined assignment (operator=)/emplace()ment of values that can be - * used to implicitly construct the underlying type, including - * direct-list-initialisation, e.g.: - * \code - * struct S { int a; char b; } - * Omittable<S> o; - * o = { 1, 'a' }; // std::optional would require o = S { 1, 'a' } - * \endcode - * - entirely deleted value(). The technical reason is that Xcode 10 doesn't - * have it; but besides that, value_or() or (after explicit checking) - * `operator*()`/`operator->()` are better alternatives within Quotient - * that doesn't practice throwing exceptions (as doesn't most of Qt). - * - disabled non-const lvalue operator*() and operator->(), as it's too easy - * to inadvertently cause a value change through them. - * - edit() to provide a safe and explicit lvalue accessor instead of those - * above. Requires the underlying type to be default-constructible. - * Allows chained initialisation of nested Omittables: - * \code - * struct Inner { int member = 10; Omittable<int> innermost; }; - * struct Outer { int anotherMember = 10; Omittable<Inner> inner; }; - * Omittable<Outer> o; // = { 10, std::nullopt }; - * o.edit().inner.edit().innermost.emplace(42); - * \endcode - * - merge() - a soft version of operator= that only overwrites its first - * operand with the second one if the second one is not empty. - */ -template <typename T> -class Omittable : public std::optional<T> { -public: - using base_type = std::optional<T>; - using value_type = std::decay_t<T>; - - using std::optional<T>::optional; - - // Overload emplace() and operator=() to allow passing braced-init-lists - // (the standard emplace() does direct-initialisation but - // not direct-list-initialisation). - using base_type::operator=; - Omittable& operator=(const value_type& v) - { - base_type::operator=(v); - return *this; - } - Omittable& operator=(value_type&& v) - { - base_type::operator=(v); - return *this; - } - using base_type::emplace; - T& emplace(const T& val) { return base_type::emplace(val); } - T& emplace(T&& val) { return base_type::emplace(std::move(val)); } - - // use value_or() or check (with operator! or has_value) before accessing - // with operator-> or operator* - // The technical reason is that Xcode 10 has incomplete std::optional - // that has no value(); but using value() may also mean that you rely - // on the optional throwing an exception (which is not assumed practice - // throughout Quotient) or that you spend unnecessary CPU cycles on - // an extraneous has_value() check. - value_type& value() = delete; - const value_type& value() const = delete; - value_type& edit() - { - return this->has_value() ? base_type::operator*() : this->emplace(); - } - - [[deprecated("Use '!o' or '!o.has_value()' instead of 'o.omitted()'")]] - bool omitted() const - { - return !this->has_value(); - } - - /// Merge the value from another Omittable - /// \return true if \p other is not omitted and the value of - /// the current Omittable was different (or omitted); - /// in other words, if the current Omittable has changed; - /// false otherwise - template <typename T1> - auto merge(const Omittable<T1>& other) - -> std::enable_if_t<std::is_convertible<T1, T>::value, bool> - { - if (!other || (this->has_value() && **this == *other)) - return false; - *this = other; - return true; - } - - // Hide non-const lvalue operator-> and operator* as these are - // a bit too surprising: value() & doesn't lazy-create an object; - // and it's too easy to inadvertently change the underlying value. - - const value_type* operator->() const& { return base_type::operator->(); } - value_type* operator->() && { return base_type::operator->(); } - const value_type& operator*() const& { return base_type::operator*(); } - value_type& operator*() && { return base_type::operator*(); } -}; - -namespace _impl { - template <typename AlwaysVoid, typename> - struct fn_traits; -} - -/// Determine traits of an arbitrary function/lambda/functor -/*! - * Doesn't work with generic lambdas and function objects that have - * operator() overloaded. - * \sa - * https://stackoverflow.com/questions/7943525/is-it-possible-to-figure-out-the-parameter-type-and-return-type-of-a-lambda#7943765 - */ -template <typename T> -struct function_traits - : public _impl::fn_traits<void, std::remove_reference_t<T>> {}; - -// Specialisation for a function -template <typename ReturnT, typename... ArgTs> -struct function_traits<ReturnT(ArgTs...)> { - using return_type = ReturnT; - using arg_types = std::tuple<ArgTs...>; - // Doesn't (and there's no plan to make it) work for "classic" - // member functions (i.e. outside of functors). - // See also the comment for wrap_in_function() below - using function_type = std::function<ReturnT(ArgTs...)>; -}; - -namespace _impl { - // Specialisation for function objects with (non-overloaded) operator() - // (this includes non-generic lambdas) - template <typename T> - struct fn_traits<decltype(void(&T::operator())), T> - : public fn_traits<void, decltype(&T::operator())> {}; - - // Specialisation for a member function - template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...)> - : function_traits<ReturnT(ArgTs...)> {}; - - // Specialisation for a const member function - template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_traits<void, ReturnT (ClassT::*)(ArgTs...) const> - : function_traits<ReturnT(ArgTs...)> {}; -} // namespace _impl - -template <typename FnT> -using fn_return_t = typename function_traits<FnT>::return_type; - -template <typename FnT, int ArgN = 0> -using fn_arg_t = - std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; - -// TODO: get rid of it as soon as Apple Clang gets proper deduction guides -// for std::function<> -// ...or consider using QtPrivate magic used by QObject::connect() -// since wrap_in_function() is actually made for qt_connection_util.h -// ...for inspiration, also check a possible std::not_fn implementation at -// https://en.cppreference.com/w/cpp/utility/functional/not_fn -template <typename FnT> -inline auto wrap_in_function(FnT&& f) -{ - return typename function_traits<FnT>::function_type(std::forward<FnT>(f)); -} - -inline auto operator"" _ls(const char* s, std::size_t size) +constexpr auto operator"" _ls(const char* s, std::size_t size) { return QLatin1String(s, int(size)); } @@ -246,6 +111,23 @@ private: iterator to; }; +template <typename T> +class asKeyValueRange +{ +public: + asKeyValueRange(T& data) + : m_data { data } + {} + + auto begin() { return m_data.keyValueBegin(); } + auto end() { return m_data.keyValueEnd(); } + +private: + T &m_data; +}; +template <typename T> +asKeyValueRange(T&) -> asKeyValueRange<T>; + /** A replica of std::find_first_of that returns a pair of iterators * * Convenient for cases when you need to know which particular "first of" @@ -253,8 +135,8 @@ private: */ template <typename InputIt, typename ForwardIt, typename Pred> inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, - ForwardIt sFirst, - ForwardIt sLast, Pred pred) + ForwardIt sFirst, + ForwardIt sLast, Pred pred) { for (; first != last; ++first) for (auto it = sFirst; it != sLast; ++it) @@ -264,27 +146,90 @@ inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, return std::make_pair(last, sLast); } +//! \brief An owning implementation pointer +//! +//! This is basically std::unique_ptr<> to hold your pimpl's but without having +//! to define default constructors/operator=() out of line. +//! Thanks to https://oliora.github.io/2015/12/29/pimpl-and-rule-of-zero.html +//! for inspiration +template <typename ImplType, typename TypeToDelete = ImplType> +using ImplPtr = std::unique_ptr<ImplType, void (*)(TypeToDelete*)>; + +// Why this works (see also the link above): because this defers the moment +// of requiring sizeof of ImplType to the place where makeImpl is invoked +// (which is located, necessarily, in the .cpp file after ImplType definition). +// The stock unique_ptr deleter (std::default_delete) normally needs sizeof +// at the same spot - as long as you defer definition of the owning type +// constructors and operator='s to the .cpp file as well. Which means you +// have to explicitly declare and define them (even if with = default), +// formally breaking the rule of zero; informally, just adding boilerplate code. +// The custom deleter itself is instantiated at makeImpl invocation - there's +// no way earlier to even know how ImplType will be deleted and whether that +// will need sizeof(ImplType) earlier. In theory it's a tad slower because +// the deleter is called by the pointer; however, the difference will not +// be noticeable (if exist at all) for any class with non-trivial contents. + +//! \brief make_unique for ImplPtr +//! +//! Since std::make_unique is not compatible with ImplPtr, this should be used +//! in constructors of frontend classes to create implementation instances. +template <typename ImplType, typename TypeToDelete = ImplType, typename... ArgTs> +inline ImplPtr<ImplType, TypeToDelete> makeImpl(ArgTs&&... args) +{ + return ImplPtr<ImplType, TypeToDelete> { + new ImplType{std::forward<ArgTs>(args)...}, + [](TypeToDelete* impl) { delete impl; } + }; +} + +template <typename ImplType, typename TypeToDelete = ImplType> +inline ImplPtr<ImplType, TypeToDelete> acquireImpl(ImplType* from) +{ + return ImplPtr<ImplType, TypeToDelete> { from, [](TypeToDelete* impl) { + delete impl; + } }; +} + +template <typename ImplType, typename TypeToDelete = ImplType> +constexpr ImplPtr<ImplType, TypeToDelete> ZeroImpl() +{ + return { nullptr, [](TypeToDelete*) { /* nullptr doesn't need deletion */ } }; +} + +//! \brief Multiplex several functors in one +//! +//! This is a well-known trick to wrap several lambdas into a single functor +//! class that can be passed to std::visit. +//! \sa https://en.cppreference.com/w/cpp/utility/variant/visit +template <typename... FunctorTs> +struct Overloads : FunctorTs... { + using FunctorTs::operator()...; +}; + +template <typename... FunctorTs> +Overloads(FunctorTs&&...) -> Overloads<FunctorTs...>; + /** Convert what looks like a URL or a Matrix ID to an HTML hyperlink */ -void linkifyUrls(QString& htmlEscapedText); +QUOTIENT_API void linkifyUrls(QString& htmlEscapedText); /** Sanitize the text before showing in HTML * * This does toHtmlEscaped() and removes Unicode BiDi marks. */ -QString sanitized(const QString& plainText); +QUOTIENT_API QString sanitized(const QString& plainText); /** Pretty-print plain text into HTML * * This includes HTML escaping of <,>,",& and calling linkifyUrls() */ -QString prettyPrint(const QString& plainText); +QUOTIENT_API QString prettyPrint(const QString& plainText); /** Return a path to cache directory after making sure that it exists * * The returned path has a trailing slash, clients don't need to append it. - * \param dir path to cache directory relative to the standard cache path + * \param dirName path to cache directory relative to the standard cache path */ -QString cacheLocation(const QString& dirName); +QUOTIENT_API QString cacheLocation(const QString& dirName); /** Hue color component of based of the hash of the string. * @@ -293,8 +238,14 @@ QString cacheLocation(const QString& dirName); * Naming and range are the same as QColor's hueF method: * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision */ -qreal stringToHueF(const QString& s); +QUOTIENT_API qreal stringToHueF(const QString& s); /** Extract the serverpart from MXID */ -QString serverPart(const QString& mxId); +QUOTIENT_API QString serverPart(const QString& mxId); + +QUOTIENT_API QString versionString(); +QUOTIENT_API int majorVersion(); +QUOTIENT_API int minorVersion(); +QUOTIENT_API int patchVersion(); +QUOTIENT_API bool encryptionSupported(); } // namespace Quotient |