diff options
Diffstat (limited to 'lib')
171 files changed, 4213 insertions, 3164 deletions
diff --git a/lib/accountregistry.cpp b/lib/accountregistry.cpp index 616b54b4..ad7c5f99 100644 --- a/lib/accountregistry.cpp +++ b/lib/accountregistry.cpp @@ -5,6 +5,7 @@ #include "accountregistry.h" #include "connection.h" +#include <QtCore/QCoreApplication> using namespace Quotient; @@ -15,14 +16,16 @@ void AccountRegistry::add(Connection* a) beginInsertRows(QModelIndex(), size(), size()); push_back(a); endInsertRows(); + emit accountCountChanged(); } void AccountRegistry::drop(Connection* a) { - const auto idx = indexOf(a); - beginRemoveRows(QModelIndex(), idx, idx); - remove(idx); - endRemoveRows(); + if (const auto idx = indexOf(a); idx != -1) { + beginRemoveRows(QModelIndex(), idx, idx); + remove(idx); + endRemoveRows(); + } Q_ASSERT(!contains(a)); } @@ -54,8 +57,6 @@ QHash<int, QByteArray> AccountRegistry::roleNames() const return { { AccountRole, "connection" } }; } - - Connection* AccountRegistry::get(const QString& userId) { for (const auto &connection : *this) { @@ -64,3 +65,73 @@ Connection* AccountRegistry::get(const QString& userId) } 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 index 2f6dffdf..9560688e 100644 --- a/lib/accountregistry.h +++ b/lib/accountregistry.h @@ -5,18 +5,35 @@ #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 const_iterator = QVector::const_iterator; - using const_reference = QVector::const_reference; + 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, @@ -26,24 +43,24 @@ public: [[deprecated("Use Accounts variable instead")]] // static AccountRegistry& instance(); - // Expose most of QVector's const-API but only provide add() and drop() + // 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 QVector<Connection*>& accounts() const { return *this; } + const vector_t& accounts() const { return *this; } void add(Connection* a); void drop(Connection* a); - const_iterator begin() const { return QVector::begin(); } - const_iterator end() const { return QVector::end(); } - const_reference front() const { return QVector::front(); } - const_reference back() const { return QVector::back(); } + 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 QVector::isEmpty, QVector::empty; - using QVector::size, QVector::count, QVector::capacity; - using QVector::cbegin, QVector::cend, QVector::contains; + 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 @@ -52,9 +69,24 @@ public: [[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 9304a3de..13de99bf 100644 --- a/lib/avatar.cpp +++ b/lib/avatar.cpp @@ -39,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; @@ -124,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, diff --git a/lib/connection.cpp b/lib/connection.cpp index 68aed4e4..3e1e556f 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -36,23 +36,21 @@ #include <variant> #ifdef Quotient_E2EE_ENABLED -# include "e2ee/qolmaccount.h" -# include "e2ee/qolmutils.h" # include "database.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" # include "keyverificationsession.h" +#endif // Quotient_E2EE_ENABLED #if QT_VERSION_MAJOR >= 6 # include <qt6keychain/keychain.h> #else # include <qt5keychain/keychain.h> #endif -#endif // Quotient_E2EE_ENABLED - -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -# include <QtCore/QCborValue> -#endif #include <QtCore/QCoreApplication> #include <QtCore/QDir> @@ -64,7 +62,6 @@ #include <QtCore/QStringBuilder> #include <QtNetwork/QDnsLookup> - using namespace Quotient; // This is very much Qt-specific; STL iterators don't have key() and value() @@ -94,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; @@ -182,15 +178,6 @@ public: void consumeToDeviceEvents(Events&& toDeviceEvents); void consumeDevicesList(DevicesList&& devicesList); - template <typename EventT> - EventT* unpackAccountData() const - { - const auto& eventIt = accountData.find(EventT::matrixTypeId()); - return eventIt == accountData.end() - ? nullptr - : weakPtrCast<EventT>(eventIt->second); - } - void packAndSendAccountData(EventPtr&& event) { const auto eventType = event->matrixType(); @@ -213,108 +200,85 @@ public: #ifdef Quotient_E2EE_ENABLED void loadSessions() { - olmSessions = q->database()->loadOlmSessions(q->picklingMode()); - } - void saveSession(QOlmSessionPtr& session, const QString &senderKey) { - auto pickleResult = session->pickle(q->picklingMode()); - if (std::holds_alternative<QOlmError>(pickleResult)) { - qCWarning(E2EE) << "Failed to pickle olm session. Error" << std::get<QOlmError>(pickleResult); - return; - } - q->database()->saveOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickleResult), QDateTime::currentDateTime()); + olmSessions = q->database()->loadOlmSessions(picklingMode); } - std::pair<QString, QString> sessionDecryptPrekey(const QOlmMessage& message, const QString &senderKey, std::unique_ptr<QOlmAccount>& olmAccount) + void saveSession(const QOlmSession& session, const QString& senderKey) const { - Q_ASSERT(message.type() == QOlmMessage::PreKey); - for (size_t i = 0; i < olmSessions[senderKey].size(); i++) { - auto& session = olmSessions[senderKey][i]; - const auto matches = session->matchesInboundSessionFrom(senderKey, message); - if(std::holds_alternative<bool>(matches) && std::get<bool>(matches)) { - qCDebug(E2EE) << "Found inbound session"; - const auto result = session->decrypt(message); - if(std::holds_alternative<QString>(result)) { - q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); - auto pickle = session->pickle(q->picklingMode()); - if (std::holds_alternative<QByteArray>(pickle)) { - q->database()->updateOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickle)); - } else { - qCWarning(E2EE) << "Failed to pickle olm session."; - } - auto s = std::move(session); - olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i); - olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s)); - return { std::get<QString>(result), olmSessions[senderKey][0]->sessionId() }; - } else { - qCDebug(E2EE) << "Failed to decrypt prekey message"; - return {}; - } - } - } - qCDebug(E2EE) << "Creating new inbound session"; - auto newSessionResult = olmAccount->createInboundSessionFrom(senderKey.toUtf8(), message); - if(std::holds_alternative<QOlmError>(newSessionResult)) { - qCWarning(E2EE) << "Failed to create inbound session for" << senderKey << std::get<QOlmError>(newSessionResult); - return {}; - } - auto newSession = std::move(std::get<QOlmSessionPtr>(newSessionResult)); - auto error = olmAccount->removeOneTimeKeys(newSession); - if (error) { - qWarning(E2EE) << "Failed to remove one time key for session" << newSession->sessionId(); - } - const auto result = newSession->decrypt(message); - QString sessionId = newSession->sessionId(); - saveSession(newSession, senderKey); - olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(newSession)); - if(std::holds_alternative<QString>(result)) { - return { std::get<QString>(result), sessionId }; - } else { - qCDebug(E2EE) << "Failed to decrypt prekey message with new session"; - return {}; - } + if (auto pickleResult = session.pickle(picklingMode)) + q->database()->saveOlmSession(senderKey, session.sessionId(), + *pickleResult, + QDateTime::currentDateTime()); + else + qCWarning(E2EE) << "Failed to pickle olm session. Error" + << pickleResult.error(); } - std::pair<QString, QString> sessionDecryptGeneral(const QOlmMessage& message, const QString &senderKey) + + template <typename FnT> + std::pair<QString, QString> doDecryptMessage(const QOlmSession& session, + const QOlmMessage& message, + FnT&& andThen) const { - Q_ASSERT(message.type() == QOlmMessage::General); - for (size_t i = 0; i < olmSessions[senderKey].size(); i++) { - auto& session = olmSessions[senderKey][i]; - const auto result = session->decrypt(message); - if(std::holds_alternative<QString>(result)) { - q->database()->setOlmSessionLastReceived(QString(session->sessionId()), QDateTime::currentDateTime()); - auto pickle = session->pickle(q->picklingMode()); - if (std::holds_alternative<QByteArray>(pickle)) { - q->database()->updateOlmSession(senderKey, session->sessionId(), std::get<QByteArray>(pickle)); - } else { - qCWarning(E2EE) << "Failed to pickle olm session."; - } - auto s = std::move(session); - olmSessions[senderKey].erase(olmSessions[senderKey].begin() + i); - olmSessions[senderKey].insert(olmSessions[senderKey].begin(), std::move(s)); - return { std::get<QString>(result), olmSessions[senderKey][0]->sessionId() }; - } + const auto expectedMessage = session.decrypt(message); + if (expectedMessage) { + const auto result = + std::make_pair(*expectedMessage, session.sessionId()); + andThen(); + return result; } - qCWarning(E2EE) << "Failed to decrypt message"; + 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, std::unique_ptr<QOlmAccount>& account) + const QJsonObject& personalCipherObject, const QByteArray& senderKey) { - QString decrypted; - QString olmSessionId; - int type = personalCipherObject.value(TypeKeyL).toInt(-1); - QByteArray body = personalCipherObject.value(BodyKeyL).toString().toLatin1(); - if (type == QOlmMessage::PreKey) { - QOlmMessage preKeyMessage(body, QOlmMessage::PreKey); - auto result = sessionDecryptPrekey(preKeyMessage, senderKey, account); - decrypted = result.first; - olmSessionId = result.second; - } else if (type == QOlmMessage::General) { - QOlmMessage message(body, QOlmMessage::General); - auto result = sessionDecryptGeneral(message, senderKey); - decrypted = result.first; - olmSessionId = result.second; + 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 {}; } - return { decrypted, olmSessionId }; + 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); + auto error = olmAccount->removeOneTimeKeys(*newSession); + if (error) { + 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 @@ -334,8 +298,9 @@ public: qCDebug(E2EE) << "Encrypted event is not for the current device"; return {}; } - const auto [decrypted, olmSessionId] = sessionDecryptMessage( - personalCipherObject, encryptedEvent.senderKey().toLatin1(), olmAccount); + const auto [decrypted, olmSessionId] = + sessionDecryptMessage(personalCipherObject, + encryptedEvent.senderKey().toLatin1()); if (decrypted.isEmpty()) { qCDebug(E2EE) << "Problem with new session from senderKey:" << encryptedEvent.senderKey() @@ -389,10 +354,53 @@ public: #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 encryptSessionKeyEvent(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(false); + 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) @@ -571,11 +579,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 + saveAccessTokenToKeychain(); +#ifdef Quotient_E2EE_ENABLED database->clear(); -#endif // Quotient_E2EE_ENABLED +#endif }); connect(loginJob, &BaseJob::failure, q, [this, loginJob] { emit q->loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -591,6 +598,7 @@ void Connection::Private::completeSetup(const QString& mxId) << "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 @@ -632,9 +640,7 @@ void Connection::Private::completeSetup(const QString& mxId) olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q); connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount); -#ifdef Quotient_E2EE_ENABLED loadSessions(); -#endif if (database->accountPickle().isEmpty()) { // create new account and save unpickle data @@ -707,7 +713,8 @@ 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 @@ -856,16 +863,14 @@ void Connection::Private::consumeRoomData(SyncDataList&& roomDataList, } 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(); } } @@ -962,38 +967,45 @@ void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents) { #ifdef Quotient_E2EE_ENABLED if (!toDeviceEvents.empty()) { - qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() << "to-device events"; - visitEach(toDeviceEvents, [this](const EncryptedEvent& event) { - if (event.algorithm() != OlmV1Curve25519AesSha2AlgoKey) { - qCDebug(E2EE) << "Unsupported algorithm" << event.id() << "for event" << event.algorithm(); - return; - } - if (q->isKnownCurveKey(event.senderId(), event.senderKey())) { - handleEncryptedToDeviceEvent(event); - return; + qCDebug(E2EE) << "Consuming" << toDeviceEvents.size() + << "to-device events"; + for (auto&& tdEvt : toDeviceEvents) { + 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)); + continue; } - trackedUsers += event.senderId(); - outdatedUsers += event.senderId(); - encryptionUpdateRequired = true; - pendingEncryptedEvents.push_back(std::make_unique<EncryptedEvent>(event.fullJson())); - }, [this](const KeyVerificationRequestEvent& event) { - auto session = new KeyVerificationSession(q->userId(), event, q, false, q); - emit q->newKeyVerificationSession(session); - }, [this](const KeyVerificationReadyEvent& event) { - emit q->incomingKeyVerificationReady(event); - }, [this](const KeyVerificationStartEvent& event) { - emit q->incomingKeyVerificationStart(event); - }, [this](const KeyVerificationAcceptEvent& event) { - emit q->incomingKeyVerificationAccept(event); - }, [this](const KeyVerificationKeyEvent& event) { - emit q->incomingKeyVerificationKey(event); - }, [this](const KeyVerificationMacEvent& event) { - emit q->incomingKeyVerificationMac(event); - }, [this](const KeyVerificationDoneEvent& event) { - emit q->incomingKeyVerificationDone(event); - }, [this](const KeyVerificationCancelEvent& event) { - emit q->incomingKeyVerificationCancel(event); - }); + switchOnType(*tdEvt, + [this](const KeyVerificationRequestEvent& event) { + auto session = new KeyVerificationSession(q->userId(), event, q, false, q); + emit q->newKeyVerificationSession(session); + }, [this](const KeyVerificationReadyEvent& event) { + emit q->incomingKeyVerificationReady(event); + }, [this](const KeyVerificationStartEvent& event) { + emit q->incomingKeyVerificationStart(event); + }, [this](const KeyVerificationAcceptEvent& event) { + emit q->incomingKeyVerificationAccept(event); + }, [this](const KeyVerificationKeyEvent& event) { + emit q->incomingKeyVerificationKey(event); + }, [this](const KeyVerificationMacEvent& event) { + emit q->incomingKeyVerificationMac(event); + }, [this](const KeyVerificationDoneEvent& event) { + emit q->incomingKeyVerificationDone(event); + }, [this](const KeyVerificationCancelEvent& event) { + emit q->incomingKeyVerificationCancel(event); + }); + } } #endif } @@ -1008,7 +1020,7 @@ void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& eve } switchOnType(*decryptedEvent, - [this, senderKey = event.senderKey(), &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { + [this, &event, olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) { if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) { detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(), olmSessionId); } else { @@ -1197,15 +1209,14 @@ DownloadFileJob* Connection::downloadFile(const QUrl& url, } #ifdef Quotient_E2EE_ENABLED -DownloadFileJob* Connection::downloadFile(const QUrl& url, - const EncryptedFile& file, - const QString& localFilename) +DownloadFileJob* Connection::downloadFile( + const QUrl& url, const EncryptedFileMetadata& fileMetadata, + const QString& localFilename) { auto mediaId = url.authority() + url.path(); auto idParts = splitMediaId(mediaId); - auto* job = - callApi<DownloadFileJob>(idParts.front(), idParts.back(), file, localFilename); - return job; + return callApi<DownloadFileJob>(idParts.front(), idParts.back(), + fileMetadata, localFilename); } #endif @@ -1377,26 +1388,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, @@ -1489,12 +1485,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); @@ -1696,7 +1694,7 @@ bool Connection::isIgnored(const User* user) const IgnoredUsersList Connection::ignoredUsers() const { - const auto* event = d->unpackAccountData<IgnoredUsersEvent>(); + const auto* event = accountData<IgnoredUsersEvent>(); return event ? event->ignored_users() : IgnoredUsersList(); } @@ -1736,7 +1734,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) @@ -1761,9 +1759,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) @@ -1850,16 +1853,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 { @@ -1912,11 +1909,11 @@ 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 { @@ -1929,15 +1926,9 @@ void Connection::saveState() const } #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()); @@ -2119,19 +2110,20 @@ void Connection::Private::loadOutdatedUserDevices() continue; } } - deviceKeys[user][device.deviceId] = device; + deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys); } outdatedUsers -= user; } saveDevicesList(); for(size_t i = 0; i < pendingEncryptedEvents.size();) { - if (q->isKnownCurveKey(pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), pendingEncryptedEvents[i]->contentJson()["sender_key"].toString())) { - handleEncryptedToDeviceEvent(*(pendingEncryptedEvents[i].get())); + if (isKnownCurveKey( + pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(), + pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) { + handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]); pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i); - } else { - i++; - } + } else + ++i; } }); } @@ -2234,101 +2226,236 @@ void Connection::saveOlmAccount() { qCDebug(E2EE) << "Saving olm account"; #ifdef Quotient_E2EE_ENABLED - auto pickle = d->olmAccount->pickle(d->picklingMode); - d->database->setAccountPickle(std::get<QByteArray>(pickle)); + if (const auto expectedPickle = d->olmAccount->pickle(d->picklingMode)) + d->database->setAccountPickle(*expectedPickle); + else + qCWarning(E2EE) << "Couldn't save Olm account pickle:" + << expectedPickle.error(); #endif } #ifdef Quotient_E2EE_ENABLED QJsonObject Connection::decryptNotification(const QJsonObject ¬ification) { - auto room = this->room(notification["room_id"].toString()); + auto r = room(notification["room_id"].toString()); auto event = makeEvent<EncryptedEvent>(notification["event"].toObject()); - auto decrypted = room->decryptMessage(*event); - if(!decrypted) { - return QJsonObject(); - } - return decrypted->fullJson(); + const auto decrypted = r->decryptMessage(*event); + return decrypted ? decrypted->fullJson() : QJsonObject(); } -Database* Connection::database() +Database* Connection::database() const { return d->database; } -UnorderedMap<QString, QOlmInboundGroupSessionPtr> Connection::loadRoomMegolmSessions(Room* room) +UnorderedMap<QString, QOlmInboundGroupSessionPtr> +Connection::loadRoomMegolmSessions(const Room* room) const { return database()->loadMegolmSessions(room->id(), picklingMode()); } -void Connection::saveMegolmSession(Room* room, QOlmInboundGroupSession* session) +void Connection::saveMegolmSession(const Room* room, + const QOlmInboundGroupSession& session) const { - database()->saveMegolmSession(room->id(), session->sessionId(), session->pickle(picklingMode()), session->senderId(), session->olmSessionId()); + database()->saveMegolmSession(room->id(), session.sessionId(), + session.pickle(picklingMode()), + session.senderId(), session.olmSessionId()); } -QStringList Connection::devicesForUser(User* user) const +QStringList Connection::devicesForUser(const QString& userId) const { - return d->deviceKeys[user->id()].keys(); + return d->deviceKeys[userId].keys(); } -QString Connection::curveKeyForUserDevice(const QString& user, const QString& device) const +QString Connection::Private::curveKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[user][device].keys["curve25519:" % device]; + return deviceKeys[userId][device].keys["curve25519:" % device]; } -QString Connection::edKeyForUserDevice(const QString& user, const QString& device) const +QString Connection::edKeyForUserDevice(const QString& userId, + const QString& device) const { - return d->deviceKeys[user][device].keys["ed25519:" % device]; + return d->deviceKeys[userId][device].keys["ed25519:" % device]; } -bool Connection::hasOlmSession(User* user, const QString& deviceId) const +bool Connection::Private::isKnownCurveKey(const QString& userId, + const QString& curveKey) const { - const auto& curveKey = curveKeyForUserDevice(user->id(), deviceId); - return d->olmSessions.contains(curveKey) && d->olmSessions[curveKey].size() > 0; + 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(); } -QPair<QOlmMessage::Type, QByteArray> Connection::olmEncryptMessage(User* user, const QString& device, const QByteArray& message) +bool Connection::hasOlmSession(const QString& user, + const QString& deviceId) const { - const auto& curveKey = curveKeyForUserDevice(user->id(), device); - QOlmMessage::Type type = d->olmSessions[curveKey][0]->encryptMessageType(); - auto result = d->olmSessions[curveKey][0]->encrypt(message); - auto pickle = d->olmSessions[curveKey][0]->pickle(picklingMode()); - if (std::holds_alternative<QByteArray>(pickle)) { - database()->updateOlmSession(curveKey, d->olmSessions[curveKey][0]->sessionId(), std::get<QByteArray>(pickle)); + 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(); + QOlmMessage::Type type = olmSession->encryptMessageType(); + const auto result = olmSession->encrypt(message); + if (const auto pickle = olmSession->pickle(picklingMode)) { + database->updateOlmSession(curveKey, olmSession->sessionId(), *pickle); } else { - qCWarning(E2EE) << "Failed to pickle olm session."; + qWarning(E2EE) << "Failed to pickle olm session: " << pickle.error(); } - return qMakePair(type, result.toCiphertext()); + return { type, result.toCiphertext() }; } -void Connection::createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey) +bool Connection::Private::createOlmSession(const QString& targetUserId, + const QString& targetDeviceId, + const OneTimeKeys& oneTimeKeyObject) { - auto session = QOlmSession::createOutboundSession(olmAccount(), theirIdentityKey, theirOneTimeKey); - if (std::holds_alternative<QOlmError>(session)) { - qCWarning(E2EE) << "Failed to create olm session for " << theirIdentityKey << std::get<QOlmError>(session); - return; + 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; } - d->saveSession(std::get<std::unique_ptr<QOlmSession>>(session), theirIdentityKey); - d->olmSessions[theirIdentityKey].push_back(std::move(std::get<std::unique_ptr<QOlmSession>>(session))); + 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); + 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::encryptSessionKeyEvent( + QJsonObject payloadJson, const QString& targetUserId, + const QString& targetDeviceId) const +{ + 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(); } -QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(Room* room) +void Connection::sendSessionKeyToDevices( + const QString& roomId, const QByteArray& sessionId, + const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices, + int index) { - return d->database->loadCurrentOutboundMegolmSession(room->id(), d->picklingMode); + 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 keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey, roomId, sessionId, + sessionKey, userId()) + .fullJson(); + keyEventJson.insert(SenderKeyL, userId()); + keyEventJson.insert("sender_device"_ls, deviceId()); + keyEventJson.insert( + "keys"_ls, + QJsonObject { + { Ed25519Key, QString(olmAccount()->identityKeys().ed25519) } }); + + auto job = callApi<ClaimKeysJob>(hash); + connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, keyEventJson, 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(); + usersToDevicesToContent[targetUserId][targetDeviceId] = + d->encryptSessionKeyEvent(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); + } + }); } -void Connection::saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data) +QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession( + const QString& roomId) const { - d->database->saveCurrentOutboundMegolmSession(room->id(), d->picklingMode, data); + return d->database->loadCurrentOutboundMegolmSession(roomId, + d->picklingMode); } -bool Connection::isKnownCurveKey(const QString& user, const QString& curveKey) +void Connection::saveCurrentOutboundMegolmSession( + const QString& roomId, const QOlmOutboundGroupSession& session) const { - auto query = database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId AND curveKey=:curveKey")); - query.bindValue(":matrixId", user); - query.bindValue(":curveKey", curveKey); - database()->execute(query); - return query.next(); + d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode, + session); } #endif @@ -2339,10 +2466,9 @@ void Connection::startKeyVerificationSession(const QString& deviceId) Q_EMIT newKeyVerificationSession(session); } -void Connection::sendToDevice(const QString& userId, const QString& deviceId, event_ptr_tt<Event> event, bool encrypted) +void Connection::sendToDevice(const QString& userId, const QString& deviceId, + event_ptr_tt<Event> event, bool encrypted) { - - UsersToDevicesToEvents payload; if (encrypted) { QJsonObject payloadJson = event->fullJson(); payloadJson["recipient"] = userId; @@ -2354,16 +2480,23 @@ void Connection::sendToDevice(const QString& userId, const QString& deviceId, ev senderObject["ed25519"] = QString(olmAccount()->identityKeys().ed25519); payloadJson["keys"] = senderObject; - const auto& u = user(userId); - auto cipherText = olmEncryptMessage(u, deviceId, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); + auto cipherText = d->olmEncryptMessage( + userId, deviceId, + QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); QJsonObject encryptedJson; - encryptedJson[curveKeyForUserDevice(userId, deviceId)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}, {"sender", this->userId()}}; - auto encryptedEvent = makeEvent<EncryptedEvent>(encryptedJson, olmAccount()->identityKeys().curve25519); - payload[userId][deviceId] = std::move(encryptedEvent); - } else { - payload[userId][deviceId] = std::move(event); - } - sendToDevices(payload[userId][deviceId]->matrixType(), payload); + encryptedJson[d->curveKeyForUserDevice(userId, deviceId)] = + QJsonObject{ { "type", cipherText.first }, + { "body", QString(cipherText.second) }, + { "sender", this->userId() } }; + const auto& contentJson = + EncryptedEvent(encryptedJson, + olmAccount()->identityKeys().curve25519) + .contentJson(); + sendToDevices(EncryptedEvent::TypeId, + { { userId, { { deviceId, contentJson } } } }); + } else + sendToDevices(event->matrixType(), + { { userId, { { deviceId, event->contentJson() } } } }); } bool Connection::isVerifiedSession(const QString& megolmSessionId) diff --git a/lib/connection.h b/lib/connection.h index fc189ac4..b684d16b 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -53,7 +53,7 @@ class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; class Database; -struct EncryptedFile; +struct EncryptedFileMetadata; class QOlmAccount; class QOlmInboundGroupSession; @@ -135,8 +135,7 @@ class QUOTIENT_API Connection : public QObject { Q_PROPERTY(bool canChangePassword READ canChangePassword NOTIFY capabilitiesLoaded) public: - using UsersToDevicesToEvents = - UnorderedMap<QString, UnorderedMap<QString, std::unique_ptr<Event>>>; + using UsersToDevicesToContent = QHash<QString, QHash<QString, QJsonObject>>; enum RoomVisibility { PublishRoom, @@ -177,24 +176,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. - */ + //! \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 <typename EventT> - const typename EventT::content_type accountData() const + 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 @@ -320,21 +320,33 @@ public: bool isLoggedIn() const; #ifdef Quotient_E2EE_ENABLED QOlmAccount* olmAccount() const; - Database* database(); - bool hasOlmSession(User* user, const QString& deviceId) 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; + - QOlmOutboundGroupSessionPtr loadCurrentOutboundMegolmSession(Room* room); - void saveCurrentOutboundMegolmSession(Room *room, const QOlmOutboundGroupSessionPtr& data); + QString edKeyForUserDevice(const QString& user, const QString& device) const; + bool hasOlmSession(const QString& user, const QString& deviceId) const; /// Returns true if this megolm session comes from a verified device bool isVerifiedSession(const QString& megolmSessionId); - //This assumes that an olm session with (user, device) exists - QPair<QOlmMessage::Type, QByteArray> olmEncryptMessage(User* user, const QString& device, const QByteArray& message); - void createOlmSession(const QString& theirIdentityKey, const QString& theirOneTimeKey); + void sendSessionKeyToDevices(const QString& roomId, + const QByteArray& sessionId, + const QByteArray& sessionKey, + const QMultiHash<QString, QString>& devices, + int index); - UnorderedMap<QString, QOlmInboundGroupSessionPtr> loadRoomMegolmSessions(Room* room); - void saveMegolmSession(Room* room, QOlmInboundGroupSession* session); + 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; @@ -607,7 +619,8 @@ public Q_SLOTS: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob* downloadFile(const QUrl& url, const EncryptedFile& file, + DownloadFileJob* downloadFile(const QUrl& url, + const EncryptedFileMetadata& fileMetadata, const QString& localFilename = {}); #endif /** @@ -687,7 +700,7 @@ public Q_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); @@ -699,13 +712,6 @@ public Q_SLOTS: #ifdef Quotient_E2EE_ENABLED void encryptionUpdate(Room *room); - PicklingMode picklingMode() const; - QJsonObject decryptNotification(const QJsonObject ¬ification); - - QStringList devicesForUser(User* user) const; - QString curveKeyForUserDevice(const QString &user, const QString& device) const; - QString edKeyForUserDevice(const QString& user, const QString& device) const; - bool isKnownCurveKey(const QString& user, const QString& curveKey); #endif Q_SIGNALS: diff --git a/lib/converters.cpp b/lib/converters.cpp index 444ca4f6..b0e3a4b6 100644 --- a/lib/converters.cpp +++ b/lib/converters.cpp @@ -2,9 +2,23 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "converters.h" +#include "logging.h" #include <QtCore/QVariant> +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 Quotient::JsonConverter<QVariant>::dump(const QVariant& v) { return QJsonValue::fromVariant(v); diff --git a/lib/converters.h b/lib/converters.h index 515c96fd..688f7bbd 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -16,6 +16,7 @@ #include <type_traits> #include <vector> +#include <variant> class QVariant; @@ -27,23 +28,19 @@ struct JsonObjectConverter { static void fillFrom(const QJsonObject&, T&) = delete; }; -namespace _impl { - template <typename T, typename = void> - struct JsonExporter { - static QJsonObject dump(const T& data) - { - QJsonObject jo; - JsonObjectConverter<T>::dumpTo(jo, data); - return jo; - } - }; +template <typename PodT, typename JsonT> +PodT fromJson(const JsonT&); - template <typename T> - struct JsonExporter< - T, std::enable_if_t<std::is_invocable_v<decltype(&T::toJson), T>>> { - static auto dump(const T& data) { return data.toJson(); } - }; -} +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(QJsonValueRef jvr) { return fromJson<T>(QJsonValue(jvr)); } + 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 //! @@ -61,13 +58,24 @@ namespace _impl { //! 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 : _impl::JsonExporter<T> { +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) + { + 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) { // 'else' below are required to suppress code generation for unused // branches - 'return' is not enough @@ -81,66 +89,143 @@ struct JsonConverter : _impl::JsonExporter<T> { 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, - typename = std::enable_if_t<!std::is_constructible_v<QJsonValue, T>>> +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; } -inline auto toJson(const QJsonValue& jv) { return jv; } - 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) { - return JsonConverter<T>::load(jv); + // JsonT here can be whatever the respective JsonConverter specialisation + // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject + return JsonConverter<PodT>::load(json); } -template<> -inline QJsonValue fromJson(const QJsonValue& jv) { return jv; } +// 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) +{ + 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; +} + +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]; + } + + _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), + qt_getEnumName(FlagT())); + Q_ASSERT(false); + return {}; } // JsonConverter<> specialisations @@ -163,6 +248,14 @@ inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } template <> inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } +//! Use fromJson<QString> and use 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 <> +inline QByteArray fromJson(const QJsonValue& jv) = delete; + template <> inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); } @@ -179,15 +272,7 @@ inline QDateTime fromJson(const QJsonValue& jv) return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); } -inline QJsonValue toJson(const QDate& val) { - return toJson( -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) - QDateTime(val) -#else - val.startOfDay() -#endif - ); -} +inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); } template <> inline QDate fromJson(const QJsonValue& jv) { @@ -216,6 +301,26 @@ struct QUOTIENT_API JsonConverter<QVariant> { 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) @@ -414,4 +519,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 09fc8d40..8c71f6c5 100644 --- a/lib/csapi/account-data.cpp +++ b/lib/csapi/account-data.cpp @@ -9,23 +9,23 @@ using namespace Quotient; SetAccountDataJob::SetAccountDataJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", type)) { - setRequestData(RequestData(toJson(content))); + setRequestData({ toJson(content) }); } QUrl GetAccountDataJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", type)); } GetAccountDataJob::GetAccountDataJob(const QString& userId, const QString& type) : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/account_data/", + makePath("/_matrix/client/v3", "/user/", userId, "/account_data/", type)) {} @@ -34,10 +34,10 @@ SetAccountDataPerRoomJob::SetAccountDataPerRoomJob(const QString& userId, const QString& type, const QJsonObject& content) : BaseJob(HttpVerb::Put, QStringLiteral("SetAccountDataPerRoomJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/account_data/", type)) { - setRequestData(RequestData(toJson(content))); + setRequestData({ toJson(content) }); } QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, @@ -46,7 +46,7 @@ QUrl GetAccountDataPerRoomJob::makeRequestUrl(QUrl baseUrl, const QString& type) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/account_data/", type)); } @@ -55,6 +55,6 @@ GetAccountDataPerRoomJob::GetAccountDataPerRoomJob(const QString& userId, const QString& roomId, const QString& type) : BaseJob(HttpVerb::Get, QStringLiteral("GetAccountDataPerRoomJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/account_data/", type)) {} diff --git a/lib/csapi/admin.cpp b/lib/csapi/admin.cpp index 81dd0624..322212db 100644 --- a/lib/csapi/admin.cpp +++ b/lib/csapi/admin.cpp @@ -9,11 +9,11 @@ using namespace Quotient; QUrl GetWhoIsJob::makeRequestUrl(QUrl baseUrl, const QString& userId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/admin/whois/", userId)); } GetWhoIsJob::GetWhoIsJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetWhoIsJob"), - makePath("/_matrix/client/r0", "/admin/whois/", userId)) + makePath("/_matrix/client/v3", "/admin/whois/", userId)) {} diff --git a/lib/csapi/administrative_contact.cpp b/lib/csapi/administrative_contact.cpp index 589c9fc1..aa55d934 100644 --- a/lib/csapi/administrative_contact.cpp +++ b/lib/csapi/administrative_contact.cpp @@ -9,59 +9,59 @@ using namespace Quotient; QUrl GetAccount3PIDsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/account/3pid")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/account/3pid")); } GetAccount3PIDsJob::GetAccount3PIDsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetAccount3PIDsJob"), - makePath("/_matrix/client/r0", "/account/3pid")) + makePath("/_matrix/client/v3", "/account/3pid")) {} Post3PIDsJob::Post3PIDsJob(const ThreePidCredentials& threePidCreds) : BaseJob(HttpVerb::Post, QStringLiteral("Post3PIDsJob"), - makePath("/_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"), - makePath("/_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"), - makePath("/_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"), - makePath("/_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"); } @@ -69,32 +69,32 @@ Unbind3pidFromAccountJob::Unbind3pidFromAccountJob(const QString& medium, const QString& address, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("Unbind3pidFromAccountJob"), - makePath("/_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"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/3pid/email/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenTo3PIDMSISDNJob::RequestTokenTo3PIDMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenTo3PIDMSISDNJob"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/3pid/msisdn/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } diff --git a/lib/csapi/administrative_contact.h b/lib/csapi/administrative_contact.h index e636b12a..27334850 100644 --- a/lib/csapi/administrative_contact.h +++ b/lib/csapi/administrative_contact.h @@ -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 <> @@ -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. @@ -295,7 +311,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. @@ -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,7 +353,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. @@ -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 40d784c6..dff7e032 100644 --- a/lib/csapi/appservice_room_directory.cpp +++ b/lib/csapi/appservice_room_directory.cpp @@ -6,14 +6,16 @@ using namespace Quotient; -UpdateAppserviceRoomDirectoryVisibilityJob::UpdateAppserviceRoomDirectoryVisibilityJob( - const QString& networkId, const QString& roomId, const QString& visibility) +UpdateAppserviceRoomDirectoryVisibilityJob:: + UpdateAppserviceRoomDirectoryVisibilityJob(const QString& networkId, + const QString& roomId, + const QString& visibility) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateAppserviceRoomDirectoryVisibilityJob"), - makePath("/_matrix/client/r0", "/directory/list/appservice/", + 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 6b2801ca..d6268979 100644 --- a/lib/csapi/appservice_room_directory.h +++ b/lib/csapi/appservice_room_directory.h @@ -21,8 +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 QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob - : public BaseJob { +class QUOTIENT_API UpdateAppserviceRoomDirectoryVisibilityJob : public BaseJob { public: /*! \brief Updates a room's visibility in the application service's room * directory. diff --git a/lib/csapi/banning.cpp b/lib/csapi/banning.cpp index 472128bb..e04075b7 100644 --- a/lib/csapi/banning.cpp +++ b/lib/csapi/banning.cpp @@ -9,21 +9,21 @@ using namespace Quotient; BanJob::BanJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("BanJob"), - makePath("/_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, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("UnbanJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/unban")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/unban")) { - 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/capabilities.cpp b/lib/csapi/capabilities.cpp index bc21e462..ca2a543f 100644 --- a/lib/csapi/capabilities.cpp +++ b/lib/csapi/capabilities.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetCapabilitiesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/capabilities")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/capabilities")); } GetCapabilitiesJob::GetCapabilitiesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetCapabilitiesJob"), - makePath("/_matrix/client/r0", "/capabilities")) + makePath("/_matrix/client/v3", "/capabilities")) { addExpectedKey("capabilities"); } diff --git a/lib/csapi/content-repo.cpp b/lib/csapi/content-repo.cpp index 6d1e38b6..6f6738af 100644 --- a/lib/csapi/content-repo.cpp +++ b/lib/csapi/content-repo.cpp @@ -16,11 +16,11 @@ auto queryToUploadContent(const QString& filename) UploadContentJob::UploadContentJob(QIODevice* content, const QString& filename, const QString& contentType) : BaseJob(HttpVerb::Post, QStringLiteral("UploadContentJob"), - makePath("/_matrix/media/r0", "/upload"), + makePath("/_matrix/media/v3", "/upload"), queryToUploadContent(filename)) { setRequestHeader("Content-Type", contentType.toLatin1()); - setRequestData(RequestData(content)); + setRequestData({ content }); addExpectedKey("content_uri"); } @@ -35,7 +35,7 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, const QString& mediaId, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/media/r0", "/download/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId), queryToGetContent(allowRemote)); } @@ -43,7 +43,7 @@ QUrl GetContentJob::makeRequestUrl(QUrl baseUrl, const QString& serverName, GetContentJob::GetContentJob(const QString& serverName, const QString& mediaId, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentJob"), - makePath("/_matrix/media/r0", "/download/", serverName, "/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId), queryToGetContent(allowRemote), {}, false) { @@ -64,7 +64,7 @@ QUrl GetContentOverrideNameJob::makeRequestUrl(QUrl baseUrl, bool allowRemote) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/media/r0", "/download/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId, "/", fileName), queryToGetContentOverrideName(allowRemote)); @@ -75,7 +75,7 @@ GetContentOverrideNameJob::GetContentOverrideNameJob(const QString& serverName, const QString& fileName, bool allowRemote) : BaseJob(HttpVerb::Get, QStringLiteral("GetContentOverrideNameJob"), - makePath("/_matrix/media/r0", "/download/", serverName, "/", + makePath("/_matrix/media/v3", "/download/", serverName, "/", mediaId, "/", fileName), queryToGetContentOverrideName(allowRemote), {}, false) { @@ -101,16 +101,17 @@ QUrl GetContentThumbnailJob::makeRequestUrl(QUrl baseUrl, { return BaseJob::makeRequestUrl( std::move(baseUrl), - makePath("/_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"), - makePath("/_matrix/media/r0", "/thumbnail/", serverName, "/", + makePath("/_matrix/media/v3", "/thumbnail/", serverName, "/", mediaId), queryToGetContentThumbnail(width, height, method, allowRemote), {}, false) @@ -130,24 +131,24 @@ QUrl GetUrlPreviewJob::makeRequestUrl(QUrl baseUrl, const QUrl& url, Omittable<qint64> ts) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/media/r0", + makePath("/_matrix/media/v3", "/preview_url"), queryToGetUrlPreview(url, ts)); } GetUrlPreviewJob::GetUrlPreviewJob(const QUrl& url, Omittable<qint64> ts) : BaseJob(HttpVerb::Get, QStringLiteral("GetUrlPreviewJob"), - makePath("/_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), - makePath("/_matrix/media/r0", "/config")); + makePath("/_matrix/media/v3", "/config")); } GetConfigJob::GetConfigJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetConfigJob"), - makePath("/_matrix/media/r0", "/config")) + makePath("/_matrix/media/v3", "/config")) {} diff --git a/lib/csapi/content-repo.h b/lib/csapi/content-repo.h index 511db985..2ba66a35 100644 --- a/lib/csapi/content-repo.h +++ b/lib/csapi/content-repo.h @@ -162,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 diff --git a/lib/csapi/create_room.cpp b/lib/csapi/create_room.cpp index 9aaef87f..afae80af 100644 --- a/lib/csapi/create_room.cpp +++ b/lib/csapi/create_room.cpp @@ -16,24 +16,26 @@ CreateRoomJob::CreateRoomJob(const QString& visibility, const QString& preset, Omittable<bool> isDirect, const QJsonObject& powerLevelContentOverride) : BaseJob(HttpVerb::Post, QStringLiteral("CreateRoomJob"), - makePath("/_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 7d566057..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: @@ -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 index 1fa0e949..83136d71 100644 --- a/lib/csapi/cross_signing.cpp +++ b/lib/csapi/cross_signing.cpp @@ -9,23 +9,25 @@ using namespace Quotient; UploadCrossSigningKeysJob::UploadCrossSigningKeysJob( const Omittable<CrossSigningKey>& masterKey, const Omittable<CrossSigningKey>& selfSigningKey, - const Omittable<CrossSigningKey>& userSigningKey) + const Omittable<CrossSigningKey>& userSigningKey, + const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Post, QStringLiteral("UploadCrossSigningKeysJob"), - makePath("/_matrix/client/r0", "/keys/device_signing/upload")) + makePath("/_matrix/client/v3", "/keys/device_signing/upload")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("master_key"), masterKey); - addParam<IfNotEmpty>(_data, QStringLiteral("self_signing_key"), + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("master_key"), masterKey); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("self_signing_key"), selfSigningKey); - addParam<IfNotEmpty>(_data, QStringLiteral("user_signing_key"), + addParam<IfNotEmpty>(_dataJson, QStringLiteral("user_signing_key"), userSigningKey); - setRequestData(std::move(_data)); + 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/r0", "/keys/signatures/upload")) + makePath("/_matrix/client/v3", "/keys/signatures/upload")) { - setRequestData(RequestData(toJson(signatures))); + setRequestData({ toJson(signatures) }); } diff --git a/lib/csapi/cross_signing.h b/lib/csapi/cross_signing.h index 617b61d1..6cea73e6 100644 --- a/lib/csapi/cross_signing.h +++ b/lib/csapi/cross_signing.h @@ -4,6 +4,7 @@ #pragma once +#include "csapi/definitions/auth_data.h" #include "csapi/definitions/cross_signing_key.h" #include "jobs/basejob.h" @@ -35,11 +36,16 @@ public: * 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<CrossSigningKey>& userSigningKey = none, + const Omittable<AuthenticationData>& auth = none); }; /*! \brief Upload cross-signing signatures. @@ -55,7 +61,7 @@ public: * The signatures to be published. */ explicit UploadCrossSigningSignaturesJob( - const QHash<QString, QHash<QString, QJsonObject>>& signatures = {}); + const QHash<QString, QHash<QString, QJsonObject>>& signatures); // Result properties 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/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 2938b4ec..d0a2595c 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; @@ -49,7 +46,6 @@ 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); @@ -64,7 +60,6 @@ struct JsonObjectConverter<PublicRoomsChunk> { } 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); @@ -77,44 +72,4 @@ struct JsonObjectConverter<PublicRoomsChunk> { } }; -/// 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); - } -}; - } // namespace Quotient 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/device_management.cpp b/lib/csapi/device_management.cpp index da6dbc76..6f2badee 100644 --- a/lib/csapi/device_management.cpp +++ b/lib/csapi/device_management.cpp @@ -9,53 +9,53 @@ using namespace Quotient; QUrl GetDevicesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/devices")); + makePath("/_matrix/client/v3", "/devices")); } GetDevicesJob::GetDevicesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetDevicesJob"), - makePath("/_matrix/client/r0", "/devices")) + makePath("/_matrix/client/v3", "/devices")) {} QUrl GetDeviceJob::makeRequestUrl(QUrl baseUrl, const QString& deviceId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/devices/", + makePath("/_matrix/client/v3", "/devices/", deviceId)); } GetDeviceJob::GetDeviceJob(const QString& deviceId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDeviceJob"), - makePath("/_matrix/client/r0", "/devices/", deviceId)) + makePath("/_matrix/client/v3", "/devices/", deviceId)) {} UpdateDeviceJob::UpdateDeviceJob(const QString& deviceId, const QString& displayName) : BaseJob(HttpVerb::Put, QStringLiteral("UpdateDeviceJob"), - makePath("/_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"), - makePath("/_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"), - makePath("/_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 430d2132..c10389b3 100644 --- a/lib/csapi/device_management.h +++ b/lib/csapi/device_management.h @@ -86,7 +86,8 @@ 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 QUOTIENT_API DeleteDeviceJob : public BaseJob { public: diff --git a/lib/csapi/directory.cpp b/lib/csapi/directory.cpp index b351b4ef..c1255bb1 100644 --- a/lib/csapi/directory.cpp +++ b/lib/csapi/directory.cpp @@ -8,48 +8,48 @@ using namespace Quotient; SetRoomAliasJob::SetRoomAliasJob(const QString& roomAlias, const QString& roomId) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomAliasJob"), - makePath("/_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), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)); } GetRoomIdByAliasJob::GetRoomIdByAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomIdByAliasJob"), - makePath("/_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), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/directory/room/", roomAlias)); } DeleteRoomAliasJob::DeleteRoomAliasJob(const QString& roomAlias) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomAliasJob"), - makePath("/_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), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases")); } GetLocalAliasesJob::GetLocalAliasesJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetLocalAliasesJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/aliases")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/aliases")) { addExpectedKey("aliases"); } diff --git a/lib/csapi/event_context.cpp b/lib/csapi/event_context.cpp index 877838e2..4ebbbf98 100644 --- a/lib/csapi/event_context.cpp +++ b/lib/csapi/event_context.cpp @@ -20,7 +20,7 @@ QUrl GetEventContextJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& filter) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/context/", eventId), queryToGetEventContext(limit, filter)); } @@ -30,7 +30,7 @@ GetEventContextJob::GetEventContextJob(const QString& roomId, Omittable<int> limit, const QString& filter) : BaseJob(HttpVerb::Get, QStringLiteral("GetEventContextJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/context/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/context/", eventId), queryToGetEventContext(limit, filter)) {} diff --git a/lib/csapi/filter.cpp b/lib/csapi/filter.cpp index 38c68be7..2469fbd1 100644 --- a/lib/csapi/filter.cpp +++ b/lib/csapi/filter.cpp @@ -8,9 +8,9 @@ using namespace Quotient; DefineFilterJob::DefineFilterJob(const QString& userId, const Filter& filter) : BaseJob(HttpVerb::Post, QStringLiteral("DefineFilterJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/filter")) + makePath("/_matrix/client/v3", "/user/", userId, "/filter")) { - setRequestData(RequestData(toJson(filter))); + setRequestData({ toJson(filter) }); addExpectedKey("filter_id"); } @@ -18,12 +18,12 @@ QUrl GetFilterJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& filterId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/filter/", filterId)); } GetFilterJob::GetFilterJob(const QString& userId, const QString& filterId) : BaseJob(HttpVerb::Get, QStringLiteral("GetFilterJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/filter/", + makePath("/_matrix/client/v3", "/user/", userId, "/filter/", filterId)) {} diff --git a/lib/csapi/inviting.cpp b/lib/csapi/inviting.cpp index 39d24611..41a8b5be 100644 --- a/lib/csapi/inviting.cpp +++ b/lib/csapi/inviting.cpp @@ -9,10 +9,10 @@ using namespace Quotient; InviteUserJob::InviteUserJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("InviteUserJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/invite")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/invite")) { - 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/inviting.h b/lib/csapi/inviting.h index 21e6cb74..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 diff --git a/lib/csapi/joining.cpp b/lib/csapi/joining.cpp index 373c1c6a..cdba95e9 100644 --- a/lib/csapi/joining.cpp +++ b/lib/csapi/joining.cpp @@ -10,13 +10,13 @@ JoinRoomByIdJob::JoinRoomByIdJob( const QString& roomId, const Omittable<ThirdPartySigned>& thirdPartySigned, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomByIdJob"), - makePath("/_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); - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); addExpectedKey("room_id"); } @@ -32,13 +32,13 @@ JoinRoomJob::JoinRoomJob(const QString& roomIdOrAlias, const Omittable<ThirdPartySigned>& thirdPartySigned, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("JoinRoomJob"), - makePath("/_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); - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - 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 f64152f7..233537bb 100644 --- a/lib/csapi/joining.h +++ b/lib/csapi/joining.h @@ -22,8 +22,8 @@ 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 QUOTIENT_API JoinRoomByIdJob : public BaseJob { public: @@ -64,8 +64,8 @@ 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 QUOTIENT_API JoinRoomJob : public BaseJob { public: diff --git a/lib/csapi/keys.cpp b/lib/csapi/keys.cpp index d6bd2fab..2e4978f2 100644 --- a/lib/csapi/keys.cpp +++ b/lib/csapi/keys.cpp @@ -7,39 +7,43 @@ 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"), - makePath("/_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"), - makePath("/_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"), - makePath("/_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"); } @@ -55,13 +59,13 @@ QUrl GetKeysChangesJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& to) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/keys/changes"), queryToGetKeysChanges(from, to)); } GetKeysChangesJob::GetKeysChangesJob(const QString& from, const QString& to) : BaseJob(HttpVerb::Get, QStringLiteral("GetKeysChangesJob"), - makePath("/_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 ce1ca9ed..2f2ebc6d 100644 --- a/lib/csapi/keys.h +++ b/lib/csapi/keys.h @@ -4,6 +4,8 @@ #pragma once +#include "e2ee/e2ee.h" + #include "csapi/definitions/cross_signing_key.h" #include "csapi/definitions/device_keys.h" @@ -30,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); @@ -207,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); } }; @@ -233,7 +256,7 @@ public: * \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. @@ -241,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 433e592c..4ca39c4c 100644 --- a/lib/csapi/kicking.cpp +++ b/lib/csapi/kicking.cpp @@ -9,10 +9,10 @@ using namespace Quotient; KickJob::KickJob(const QString& roomId, const QString& userId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("KickJob"), - makePath("/_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/knocking.cpp b/lib/csapi/knocking.cpp index 73e13e6e..b9da4b9b 100644 --- a/lib/csapi/knocking.cpp +++ b/lib/csapi/knocking.cpp @@ -16,11 +16,11 @@ auto queryToKnockRoom(const QStringList& serverName) KnockRoomJob::KnockRoomJob(const QString& roomIdOrAlias, const QStringList& serverName, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("KnockRoomJob"), - makePath("/_matrix/client/r0", "/knock/", roomIdOrAlias), + makePath("/_matrix/client/v3", "/knock/", roomIdOrAlias), queryToKnockRoom(serverName)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + 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 index e3645b59..f43033a8 100644 --- a/lib/csapi/knocking.h +++ b/lib/csapi/knocking.h @@ -25,7 +25,7 @@ namespace Quotient { * history visibility to the user. * * The knock will appear as an entry in the response of the - * [`/sync`](/client-server-api/#get_matrixclientr0sync) API. + * [`/sync`](/client-server-api/#get_matrixclientv3sync) API. */ class QUOTIENT_API KnockRoomJob : public BaseJob { public: diff --git a/lib/csapi/leaving.cpp b/lib/csapi/leaving.cpp index 0e5386be..ba91f26a 100644 --- a/lib/csapi/leaving.cpp +++ b/lib/csapi/leaving.cpp @@ -8,21 +8,21 @@ using namespace Quotient; LeaveRoomJob::LeaveRoomJob(const QString& roomId, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("LeaveRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/leave")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/leave")) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("reason"), reason); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<IfNotEmpty>(_dataJson, QStringLiteral("reason"), reason); + setRequestData({ _dataJson }); } QUrl ForgetRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget")); } ForgetRoomJob::ForgetRoomJob(const QString& roomId) : BaseJob(HttpVerb::Post, QStringLiteral("ForgetRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/forget")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/forget")) {} diff --git a/lib/csapi/list_joined_rooms.cpp b/lib/csapi/list_joined_rooms.cpp index 22ba04da..cdcf3eb2 100644 --- a/lib/csapi/list_joined_rooms.cpp +++ b/lib/csapi/list_joined_rooms.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetJoinedRoomsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/joined_rooms")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/joined_rooms")); } GetJoinedRoomsJob::GetJoinedRoomsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedRoomsJob"), - makePath("/_matrix/client/r0", "/joined_rooms")) + makePath("/_matrix/client/v3", "/joined_rooms")) { addExpectedKey("joined_rooms"); } diff --git a/lib/csapi/list_public_rooms.cpp b/lib/csapi/list_public_rooms.cpp index 25f8da5c..4deecfc2 100644 --- a/lib/csapi/list_public_rooms.cpp +++ b/lib/csapi/list_public_rooms.cpp @@ -10,25 +10,25 @@ QUrl GetRoomVisibilityOnDirectoryJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/directory/list/room/", roomId)); } GetRoomVisibilityOnDirectoryJob::GetRoomVisibilityOnDirectoryJob( const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomVisibilityOnDirectoryJob"), - makePath("/_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"), - makePath("/_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, @@ -46,7 +46,7 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit, const QString& server) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/publicRooms"), queryToGetPublicRooms(limit, since, server)); } @@ -54,7 +54,7 @@ QUrl GetPublicRoomsJob::makeRequestUrl(QUrl baseUrl, Omittable<int> limit, GetPublicRoomsJob::GetPublicRoomsJob(Omittable<int> limit, const QString& since, const QString& server) : BaseJob(HttpVerb::Get, QStringLiteral("GetPublicRoomsJob"), - makePath("/_matrix/client/r0", "/publicRooms"), + makePath("/_matrix/client/v3", "/publicRooms"), queryToGetPublicRooms(limit, since, server), {}, false) { addExpectedKey("chunk"); @@ -74,17 +74,17 @@ QueryPublicRoomsJob::QueryPublicRoomsJob(const QString& server, Omittable<bool> includeAllNetworks, const QString& thirdPartyInstanceId) : BaseJob(HttpVerb::Post, QStringLiteral("QueryPublicRoomsJob"), - makePath("/_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/login.cpp b/lib/csapi/login.cpp index 71fd93c5..81e603b5 100644 --- a/lib/csapi/login.cpp +++ b/lib/csapi/login.cpp @@ -9,29 +9,33 @@ using namespace Quotient; QUrl GetLoginFlowsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/login")); + makePath("/_matrix/client/v3", "/login")); } GetLoginFlowsJob::GetLoginFlowsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetLoginFlowsJob"), - makePath("/_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"), - makePath("/_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 }); } diff --git a/lib/csapi/login.h b/lib/csapi/login.h index ce6951eb..b9f14266 100644 --- a/lib/csapi/login.h +++ b/lib/csapi/login.h @@ -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 e8083e31..9ec54c71 100644 --- a/lib/csapi/logout.cpp +++ b/lib/csapi/logout.cpp @@ -9,21 +9,21 @@ using namespace Quotient; QUrl LogoutJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/logout")); + makePath("/_matrix/client/v3", "/logout")); } LogoutJob::LogoutJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutJob"), - makePath("/_matrix/client/r0", "/logout")) + makePath("/_matrix/client/v3", "/logout")) {} QUrl LogoutAllJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/logout/all")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/logout/all")); } LogoutAllJob::LogoutAllJob() : BaseJob(HttpVerb::Post, QStringLiteral("LogoutAllJob"), - makePath("/_matrix/client/r0", "/logout/all")) + makePath("/_matrix/client/v3", "/logout/all")) {} diff --git a/lib/csapi/message_pagination.cpp b/lib/csapi/message_pagination.cpp index 1a93b75b..0b2c99ce 100644 --- a/lib/csapi/message_pagination.cpp +++ b/lib/csapi/message_pagination.cpp @@ -11,7 +11,7 @@ auto queryToGetRoomEvents(const QString& from, const QString& to, const QString& filter) { QUrlQuery _q; - addParam<>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); addParam<>(_q, QStringLiteral("dir"), dir); addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); @@ -20,20 +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), - makePath("/_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"), - makePath("/_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 8c18f104..9831ae2d 100644 --- a/lib/csapi/message_pagination.h +++ b/lib/csapi/message_pagination.h @@ -25,20 +25,30 @@ public: * \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 1e523c6f..38aed174 100644 --- a/lib/csapi/notifications.cpp +++ b/lib/csapi/notifications.cpp @@ -21,7 +21,7 @@ QUrl GetNotificationsJob::makeRequestUrl(QUrl baseUrl, const QString& from, const QString& only) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/notifications"), queryToGetNotifications(from, limit, only)); } @@ -30,7 +30,7 @@ GetNotificationsJob::GetNotificationsJob(const QString& from, Omittable<int> limit, const QString& only) : BaseJob(HttpVerb::Get, QStringLiteral("GetNotificationsJob"), - makePath("/_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 23211758..48167877 100644 --- a/lib/csapi/notifications.h +++ b/lib/csapi/notifications.h @@ -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 5c93a2d7..7e89b8a6 100644 --- a/lib/csapi/openid.cpp +++ b/lib/csapi/openid.cpp @@ -9,8 +9,8 @@ using namespace Quotient; RequestOpenIdTokenJob::RequestOpenIdTokenJob(const QString& userId, const QJsonObject& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestOpenIdTokenJob"), - makePath("/_matrix/client/r0", "/user/", userId, + makePath("/_matrix/client/v3", "/user/", userId, "/openid/request_token")) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } diff --git a/lib/csapi/openid.h b/lib/csapi/openid.h index 773b6011..b3f72a25 100644 --- a/lib/csapi/openid.h +++ b/lib/csapi/openid.h @@ -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 eb5d22fa..9dd1445e 100644 --- a/lib/csapi/peeking_events.cpp +++ b/lib/csapi/peeking_events.cpp @@ -20,13 +20,13 @@ QUrl PeekEventsJob::makeRequestUrl(QUrl baseUrl, const QString& from, Omittable<int> timeout, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_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"), - makePath("/_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 14cb6f0b..ff688c49 100644 --- a/lib/csapi/peeking_events.h +++ b/lib/csapi/peeking_events.h @@ -9,7 +9,7 @@ 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 @@ -24,7 +24,7 @@ namespace Quotient { */ 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 4f77c466..828ccfb7 100644 --- a/lib/csapi/presence.cpp +++ b/lib/csapi/presence.cpp @@ -9,24 +9,24 @@ using namespace Quotient; SetPresenceJob::SetPresenceJob(const QString& userId, const QString& presence, const QString& statusMsg) : BaseJob(HttpVerb::Put, QStringLiteral("SetPresenceJob"), - makePath("/_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), - makePath("/_matrix/client/r0", "/presence/", + makePath("/_matrix/client/v3", "/presence/", userId, "/status")); } GetPresenceJob::GetPresenceJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPresenceJob"), - makePath("/_matrix/client/r0", "/presence/", userId, "/status")) + makePath("/_matrix/client/v3", "/presence/", userId, "/status")) { addExpectedKey("presence"); } diff --git a/lib/csapi/profile.cpp b/lib/csapi/profile.cpp index 64ac84ca..f024ed82 100644 --- a/lib/csapi/profile.cpp +++ b/lib/csapi/profile.cpp @@ -9,56 +9,58 @@ using namespace Quotient; SetDisplayNameJob::SetDisplayNameJob(const QString& userId, const QString& displayname) : BaseJob(HttpVerb::Put, QStringLiteral("SetDisplayNameJob"), - makePath("/_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), - makePath("/_matrix/client/r0", "/profile/", + makePath("/_matrix/client/v3", "/profile/", userId, "/displayname")); } GetDisplayNameJob::GetDisplayNameJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetDisplayNameJob"), - makePath("/_matrix/client/r0", "/profile/", userId, "/displayname"), + makePath("/_matrix/client/v3", "/profile/", userId, + "/displayname"), false) {} SetAvatarUrlJob::SetAvatarUrlJob(const QString& userId, const QUrl& avatarUrl) : BaseJob(HttpVerb::Put, QStringLiteral("SetAvatarUrlJob"), - makePath("/_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), - makePath("/_matrix/client/r0", "/profile/", + makePath("/_matrix/client/v3", "/profile/", userId, "/avatar_url")); } GetAvatarUrlJob::GetAvatarUrlJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetAvatarUrlJob"), - makePath("/_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), - makePath("/_matrix/client/r0", "/profile/", + makePath("/_matrix/client/v3", "/profile/", userId)); } GetUserProfileJob::GetUserProfileJob(const QString& userId) : BaseJob(HttpVerb::Get, QStringLiteral("GetUserProfileJob"), - makePath("/_matrix/client/r0", "/profile/", userId), false) + makePath("/_matrix/client/v3", "/profile/", userId), false) {} diff --git a/lib/csapi/pusher.cpp b/lib/csapi/pusher.cpp index ef4b3767..fb6595fc 100644 --- a/lib/csapi/pusher.cpp +++ b/lib/csapi/pusher.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetPushersJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushers")); + makePath("/_matrix/client/v3", "/pushers")); } GetPushersJob::GetPushersJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushersJob"), - makePath("/_matrix/client/r0", "/pushers")) + makePath("/_matrix/client/v3", "/pushers")) {} PostPusherJob::PostPusherJob(const QString& pushkey, const QString& kind, @@ -23,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"), - makePath("/_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/pushrules.cpp b/lib/csapi/pushrules.cpp index 0d840788..2376654a 100644 --- a/lib/csapi/pushrules.cpp +++ b/lib/csapi/pushrules.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetPushRulesJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/pushrules")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/pushrules")); } GetPushRulesJob::GetPushRulesJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRulesJob"), - makePath("/_matrix/client/r0", "/pushrules")) + makePath("/_matrix/client/v3", "/pushrules")) { addExpectedKey("global"); } @@ -23,14 +23,14 @@ QUrl GetPushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& kind, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)); } GetPushRuleJob::GetPushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)) {} @@ -39,14 +39,14 @@ QUrl DeletePushRuleJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)); } DeletePushRuleJob::DeletePushRuleJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Delete, QStringLiteral("DeletePushRuleJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId)) {} @@ -65,15 +65,15 @@ SetPushRuleJob::SetPushRuleJob(const QString& scope, const QString& kind, const QVector<PushCondition>& conditions, const QString& pattern) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + 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, @@ -81,7 +81,7 @@ QUrl IsPushRuleEnabledJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/enabled")); } @@ -90,7 +90,7 @@ IsPushRuleEnabledJob::IsPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("IsPushRuleEnabledJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/enabled")) { addExpectedKey("enabled"); @@ -100,12 +100,12 @@ SetPushRuleEnabledJob::SetPushRuleEnabledJob(const QString& scope, const QString& kind, const QString& ruleId, bool enabled) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleEnabledJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + 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, @@ -113,7 +113,7 @@ QUrl GetPushRuleActionsJob::makeRequestUrl(QUrl baseUrl, const QString& scope, const QString& ruleId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/pushrules/", + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/actions")); } @@ -122,7 +122,7 @@ GetPushRuleActionsJob::GetPushRuleActionsJob(const QString& scope, const QString& kind, const QString& ruleId) : BaseJob(HttpVerb::Get, QStringLiteral("GetPushRuleActionsJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + makePath("/_matrix/client/v3", "/pushrules/", scope, "/", kind, "/", ruleId, "/actions")) { addExpectedKey("actions"); @@ -133,10 +133,10 @@ SetPushRuleActionsJob::SetPushRuleActionsJob(const QString& scope, const QString& ruleId, const QVector<QVariant>& actions) : BaseJob(HttpVerb::Put, QStringLiteral("SetPushRuleActionsJob"), - makePath("/_matrix/client/r0", "/pushrules/", scope, "/", kind, + 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/read_markers.cpp b/lib/csapi/read_markers.cpp index f2edb71e..de5f4a9a 100644 --- a/lib/csapi/read_markers.cpp +++ b/lib/csapi/read_markers.cpp @@ -10,10 +10,10 @@ SetReadMarkerJob::SetReadMarkerJob(const QString& roomId, const QString& mFullyRead, const QString& mRead) : BaseJob(HttpVerb::Post, QStringLiteral("SetReadMarkerJob"), - makePath("/_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<>(_dataJson, QStringLiteral("m.fully_read"), mFullyRead); + addParam<IfNotEmpty>(_dataJson, QStringLiteral("m.read"), mRead); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/receipts.cpp b/lib/csapi/receipts.cpp index 401c3bfe..0194603d 100644 --- a/lib/csapi/receipts.cpp +++ b/lib/csapi/receipts.cpp @@ -10,8 +10,8 @@ PostReceiptJob::PostReceiptJob(const QString& roomId, const QString& receiptType const QString& eventId, const QJsonObject& receipt) : BaseJob(HttpVerb::Post, QStringLiteral("PostReceiptJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/receipt/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/receipt/", receiptType, "/", eventId)) { - setRequestData(RequestData(toJson(receipt))); + setRequestData({ toJson(receipt) }); } diff --git a/lib/csapi/redaction.cpp b/lib/csapi/redaction.cpp index acf1b0e4..154abd9b 100644 --- a/lib/csapi/redaction.cpp +++ b/lib/csapi/redaction.cpp @@ -9,10 +9,10 @@ using namespace Quotient; RedactEventJob::RedactEventJob(const QString& roomId, const QString& eventId, const QString& txnId, const QString& reason) : BaseJob(HttpVerb::Put, QStringLiteral("RedactEventJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/redact/", + 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/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 153abcee..04c0fe12 100644 --- a/lib/csapi/registration.cpp +++ b/lib/csapi/registration.cpp @@ -18,85 +18,91 @@ 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"), - makePath("/_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"), - makePath("/_matrix/client/r0", "/register/email/requestToken"), + makePath("/_matrix/client/v3", "/register/email/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenToRegisterMSISDNJob::RequestTokenToRegisterMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToRegisterMSISDNJob"), - makePath("/_matrix/client/r0", "/register/msisdn/requestToken"), + makePath("/_matrix/client/v3", "/register/msisdn/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } ChangePasswordJob::ChangePasswordJob(const QString& newPassword, bool logoutDevices, const Omittable<AuthenticationData>& auth) : BaseJob(HttpVerb::Post, QStringLiteral("ChangePasswordJob"), - makePath("/_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"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/password/email/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } RequestTokenToResetPasswordMSISDNJob::RequestTokenToResetPasswordMSISDNJob( const MsisdnValidationData& body) : BaseJob(HttpVerb::Post, QStringLiteral("RequestTokenToResetPasswordMSISDNJob"), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/account/password/msisdn/requestToken"), false) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); } DeactivateAccountJob::DeactivateAccountJob( const Omittable<AuthenticationData>& auth, const QString& idServer) : BaseJob(HttpVerb::Post, QStringLiteral("DeactivateAccountJob"), - makePath("/_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"); } @@ -111,13 +117,14 @@ QUrl CheckUsernameAvailabilityJob::makeRequestUrl(QUrl baseUrl, const QString& username) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/register/available"), queryToCheckUsernameAvailability(username)); } -CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob(const QString& username) +CheckUsernameAvailabilityJob::CheckUsernameAvailabilityJob( + const QString& username) : BaseJob(HttpVerb::Get, QStringLiteral("CheckUsernameAvailabilityJob"), - makePath("/_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 10375971..21d7f9d7 100644 --- a/lib/csapi/registration.h +++ b/lib/csapi/registration.h @@ -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 @@ -227,7 +243,8 @@ 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 @@ -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 @@ -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 @@ -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); @@ -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 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..8bcecee4 --- /dev/null +++ b/lib/csapi/relations.cpp @@ -0,0 +1,111 @@ +/****************************************************************************** + * 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) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + return _q; +} + +QUrl GetRelatingEventsJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, + const QString& eventId, + const QString& from, const QString& to, + Omittable<int> limit) +{ + return BaseJob::makeRequestUrl(std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", + roomId, "/relations/", eventId), + queryToGetRelatingEvents(from, to, limit)); +} + +GetRelatingEventsJob::GetRelatingEventsJob(const QString& roomId, + const QString& eventId, + const QString& from, + const QString& to, + Omittable<int> limit) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId), + queryToGetRelatingEvents(from, to, limit)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelType(const QString& from, const QString& to, + Omittable<int> limit) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + 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) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit)); +} + +GetRelatingEventsWithRelTypeJob::GetRelatingEventsWithRelTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& from, const QString& to, Omittable<int> limit) + : BaseJob(HttpVerb::Get, QStringLiteral("GetRelatingEventsWithRelTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType), + queryToGetRelatingEventsWithRelType(from, to, limit)) +{ + addExpectedKey("chunk"); +} + +auto queryToGetRelatingEventsWithRelTypeAndEventType(const QString& from, + const QString& to, + Omittable<int> limit) +{ + QUrlQuery _q; + addParam<IfNotEmpty>(_q, QStringLiteral("from"), from); + addParam<IfNotEmpty>(_q, QStringLiteral("to"), to); + addParam<IfNotEmpty>(_q, QStringLiteral("limit"), limit); + 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) +{ + return BaseJob::makeRequestUrl( + std::move(baseUrl), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit)); +} + +GetRelatingEventsWithRelTypeAndEventTypeJob:: + GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from, const QString& to, + Omittable<int> limit) + : BaseJob(HttpVerb::Get, + QStringLiteral("GetRelatingEventsWithRelTypeAndEventTypeJob"), + makePath("/_matrix/client/v1", "/rooms/", roomId, "/relations/", + eventId, "/", relType, "/", eventType), + queryToGetRelatingEventsWithRelTypeAndEventType(from, to, limit)) +{ + addExpectedKey("chunk"); +} diff --git a/lib/csapi/relations.h b/lib/csapi/relations.h new file mode 100644 index 00000000..985a43b5 --- /dev/null +++ b/lib/csapi/relations.h @@ -0,0 +1,277 @@ +/****************************************************************************** + * THIS FILE IS GENERATED - ANY EDITS WILL BE OVERWRITTEN + */ + +#pragma once + +#include "events/eventloader.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` 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. + */ + explicit GetRelatingEventsJob(const QString& roomId, const QString& eventId, + const QString& from = {}, + const QString& to = {}, + Omittable<int> limit = none); + + /*! \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); + + // 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` 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. + */ + explicit GetRelatingEventsWithRelTypeJob(const QString& roomId, + const QString& eventId, + const QString& relType, + const QString& from = {}, + const QString& to = {}, + Omittable<int> limit = none); + + /*! \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); + + // 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` 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. + */ + explicit GetRelatingEventsWithRelTypeAndEventTypeJob( + const QString& roomId, const QString& eventId, const QString& relType, + const QString& eventType, const QString& from = {}, + const QString& to = {}, Omittable<int> limit = none); + + /*! \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); + + // 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 0a76d5b8..bc52208f 100644 --- a/lib/csapi/report_content.cpp +++ b/lib/csapi/report_content.cpp @@ -9,11 +9,11 @@ using namespace Quotient; ReportContentJob::ReportContentJob(const QString& roomId, const QString& eventId, Omittable<int> score, const QString& reason) : BaseJob(HttpVerb::Post, QStringLiteral("ReportContentJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/report/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/report/", eventId)) { - QJsonObject _data; - addParam<IfNotEmpty>(_data, QStringLiteral("score"), score); - addParam<IfNotEmpty>(_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/room_send.cpp b/lib/csapi/room_send.cpp index f80f9300..2319496f 100644 --- a/lib/csapi/room_send.cpp +++ b/lib/csapi/room_send.cpp @@ -9,9 +9,9 @@ using namespace Quotient; SendMessageJob::SendMessageJob(const QString& roomId, const QString& eventType, const QString& txnId, const QJsonObject& body) : BaseJob(HttpVerb::Put, QStringLiteral("SendMessageJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/send/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/send/", eventType, "/", txnId)) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_send.h b/lib/csapi/room_send.h index fea3d59d..fcb6b24f 100644 --- a/lib/csapi/room_send.h +++ b/lib/csapi/room_send.h @@ -16,7 +16,8 @@ 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 QUOTIENT_API SendMessageJob : public BaseJob { public: diff --git a/lib/csapi/room_state.cpp b/lib/csapi/room_state.cpp index f6d2e6ec..b4adb739 100644 --- a/lib/csapi/room_state.cpp +++ b/lib/csapi/room_state.cpp @@ -11,9 +11,9 @@ SetRoomStateWithKeyJob::SetRoomStateWithKeyJob(const QString& roomId, const QString& stateKey, const QJsonObject& body) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomStateWithKeyJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", eventType, "/", stateKey)) { - setRequestData(RequestData(toJson(body))); + setRequestData({ toJson(body) }); addExpectedKey("event_id"); } diff --git a/lib/csapi/room_upgrades.cpp b/lib/csapi/room_upgrades.cpp index d4129cfb..b03fb6e8 100644 --- a/lib/csapi/room_upgrades.cpp +++ b/lib/csapi/room_upgrades.cpp @@ -8,10 +8,10 @@ using namespace Quotient; UpgradeRoomJob::UpgradeRoomJob(const QString& roomId, const QString& newVersion) : BaseJob(HttpVerb::Post, QStringLiteral("UpgradeRoomJob"), - makePath("/_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/rooms.cpp b/lib/csapi/rooms.cpp index 5310aa32..563f4fa5 100644 --- a/lib/csapi/rooms.cpp +++ b/lib/csapi/rooms.cpp @@ -10,14 +10,14 @@ QUrl GetOneRoomEventJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& eventId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/event/", eventId)); } GetOneRoomEventJob::GetOneRoomEventJob(const QString& roomId, const QString& eventId) : BaseJob(HttpVerb::Get, QStringLiteral("GetOneRoomEventJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/event/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/event/", eventId)) {} @@ -26,7 +26,7 @@ QUrl GetRoomStateWithKeyJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, const QString& stateKey) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", eventType, "/", stateKey)); } @@ -35,20 +35,20 @@ GetRoomStateWithKeyJob::GetRoomStateWithKeyJob(const QString& roomId, const QString& eventType, const QString& stateKey) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateWithKeyJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/state/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state/", eventType, "/", stateKey)) {} QUrl GetRoomStateJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state")); } GetRoomStateJob::GetRoomStateJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomStateJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/state")) + makePath("/_matrix/client/v3", "/rooms/", roomId, "/state")) {} auto queryToGetMembersByRoom(const QString& at, const QString& membership, @@ -68,7 +68,7 @@ QUrl GetMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId, { return BaseJob::makeRequestUrl( std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)); } @@ -77,7 +77,7 @@ GetMembersByRoomJob::GetMembersByRoomJob(const QString& roomId, const QString& membership, const QString& notMembership) : BaseJob(HttpVerb::Get, QStringLiteral("GetMembersByRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/members"), + makePath("/_matrix/client/v3", "/rooms/", roomId, "/members"), queryToGetMembersByRoom(at, membership, notMembership)) {} @@ -85,12 +85,12 @@ QUrl GetJoinedMembersByRoomJob::makeRequestUrl(QUrl baseUrl, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/rooms/", + makePath("/_matrix/client/v3", "/rooms/", roomId, "/joined_members")); } GetJoinedMembersByRoomJob::GetJoinedMembersByRoomJob(const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetJoinedMembersByRoomJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, + makePath("/_matrix/client/v3", "/rooms/", roomId, "/joined_members")) {} diff --git a/lib/csapi/rooms.h b/lib/csapi/rooms.h index f0815109..247fb13f 100644 --- a/lib/csapi/rooms.h +++ b/lib/csapi/rooms.h @@ -5,7 +5,6 @@ #pragma once #include "events/eventloader.h" -#include "events/roommemberevent.h" #include "jobs/basejob.h" namespace Quotient { @@ -38,7 +37,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. @@ -146,10 +145,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,9 +153,8 @@ 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 QUOTIENT_API GetJoinedMembersByRoomJob : public BaseJob { public: diff --git a/lib/csapi/search.cpp b/lib/csapi/search.cpp index 295dd1cc..4e2c9e92 100644 --- a/lib/csapi/search.cpp +++ b/lib/csapi/search.cpp @@ -16,11 +16,11 @@ auto queryToSearch(const QString& nextBatch) SearchJob::SearchJob(const Categories& searchCategories, const QString& nextBatch) : BaseJob(HttpVerb::Post, QStringLiteral("SearchJob"), - makePath("/_matrix/client/r0", "/search"), + makePath("/_matrix/client/v3", "/search"), queryToSearch(nextBatch)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("search_categories"), searchCategories); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("search_categories"), searchCategories); + setRequestData({ _dataJson }); addExpectedKey("search_categories"); } 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..7a421be8 --- /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/eventloader.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 871d6ff6..71f8147c 100644 --- a/lib/csapi/sso_login_redirect.cpp +++ b/lib/csapi/sso_login_redirect.cpp @@ -16,14 +16,14 @@ auto queryToRedirectToSSO(const QString& redirectUrl) QUrl RedirectToSSOJob::makeRequestUrl(QUrl baseUrl, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl)); } RedirectToSSOJob::RedirectToSSOJob(const QString& redirectUrl) : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToSSOJob"), - makePath("/_matrix/client/r0", "/login/sso/redirect"), + makePath("/_matrix/client/v3", "/login/sso/redirect"), queryToRedirectToSSO(redirectUrl), {}, false) {} @@ -38,7 +38,7 @@ QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId, const QString& redirectUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId), queryToRedirectToIdP(redirectUrl)); } @@ -46,6 +46,6 @@ QUrl RedirectToIdPJob::makeRequestUrl(QUrl baseUrl, const QString& idpId, RedirectToIdPJob::RedirectToIdPJob(const QString& idpId, const QString& redirectUrl) : BaseJob(HttpVerb::Get, QStringLiteral("RedirectToIdPJob"), - makePath("/_matrix/client/r0", "/login/sso/redirect/", idpId), + makePath("/_matrix/client/v3", "/login/sso/redirect/", idpId), queryToRedirectToIdP(redirectUrl), {}, false) {} diff --git a/lib/csapi/tags.cpp b/lib/csapi/tags.cpp index f717de6e..2c85842d 100644 --- a/lib/csapi/tags.cpp +++ b/lib/csapi/tags.cpp @@ -10,13 +10,13 @@ QUrl GetRoomTagsJob::makeRequestUrl(QUrl baseUrl, const QString& userId, const QString& roomId) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags")); } GetRoomTagsJob::GetRoomTagsJob(const QString& userId, const QString& roomId) : BaseJob(HttpVerb::Get, QStringLiteral("GetRoomTagsJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags")) {} @@ -24,20 +24,20 @@ SetRoomTagJob::SetRoomTagJob(const QString& userId, const QString& roomId, const QString& tag, Omittable<float> order, const QVariantHash& additionalProperties) : BaseJob(HttpVerb::Put, QStringLiteral("SetRoomTagJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + 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), - makePath("/_matrix/client/r0", "/user/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags/", tag)); } @@ -45,6 +45,6 @@ QUrl DeleteRoomTagJob::makeRequestUrl(QUrl baseUrl, const QString& userId, DeleteRoomTagJob::DeleteRoomTagJob(const QString& userId, const QString& roomId, const QString& tag) : BaseJob(HttpVerb::Delete, QStringLiteral("DeleteRoomTagJob"), - makePath("/_matrix/client/r0", "/user/", userId, "/rooms/", + makePath("/_matrix/client/v3", "/user/", userId, "/rooms/", roomId, "/tags/", tag)) {} diff --git a/lib/csapi/third_party_lookup.cpp b/lib/csapi/third_party_lookup.cpp index 4c930668..1e5870ce 100644 --- a/lib/csapi/third_party_lookup.cpp +++ b/lib/csapi/third_party_lookup.cpp @@ -9,26 +9,26 @@ using namespace Quotient; QUrl GetProtocolsJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/protocols")); } GetProtocolsJob::GetProtocolsJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolsJob"), - makePath("/_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), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/protocol/", protocol)); } GetProtocolMetadataJob::GetProtocolMetadataJob(const QString& protocol) : BaseJob(HttpVerb::Get, QStringLiteral("GetProtocolMetadataJob"), - makePath("/_matrix/client/r0", "/thirdparty/protocol/", protocol)) + makePath("/_matrix/client/v3", "/thirdparty/protocol/", protocol)) {} auto queryToQueryLocationByProtocol(const QString& searchFields) @@ -43,7 +43,7 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& searchFields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)); } @@ -51,7 +51,7 @@ QUrl QueryLocationByProtocolJob::makeRequestUrl(QUrl baseUrl, QueryLocationByProtocolJob::QueryLocationByProtocolJob( const QString& protocol, const QString& searchFields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByProtocolJob"), - makePath("/_matrix/client/r0", "/thirdparty/location/", protocol), + makePath("/_matrix/client/v3", "/thirdparty/location/", protocol), queryToQueryLocationByProtocol(searchFields)) {} @@ -67,7 +67,7 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, const QString& fields) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)); } @@ -75,7 +75,7 @@ QUrl QueryUserByProtocolJob::makeRequestUrl(QUrl baseUrl, QueryUserByProtocolJob::QueryUserByProtocolJob(const QString& protocol, const QString& fields) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByProtocolJob"), - makePath("/_matrix/client/r0", "/thirdparty/user/", protocol), + makePath("/_matrix/client/v3", "/thirdparty/user/", protocol), queryToQueryUserByProtocol(fields)) {} @@ -89,14 +89,14 @@ auto queryToQueryLocationByAlias(const QString& alias) QUrl QueryLocationByAliasJob::makeRequestUrl(QUrl baseUrl, const QString& alias) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/location"), queryToQueryLocationByAlias(alias)); } QueryLocationByAliasJob::QueryLocationByAliasJob(const QString& alias) : BaseJob(HttpVerb::Get, QStringLiteral("QueryLocationByAliasJob"), - makePath("/_matrix/client/r0", "/thirdparty/location"), + makePath("/_matrix/client/v3", "/thirdparty/location"), queryToQueryLocationByAlias(alias)) {} @@ -110,13 +110,13 @@ auto queryToQueryUserByID(const QString& userid) QUrl QueryUserByIDJob::makeRequestUrl(QUrl baseUrl, const QString& userid) { return BaseJob::makeRequestUrl(std::move(baseUrl), - makePath("/_matrix/client/r0", + makePath("/_matrix/client/v3", "/thirdparty/user"), queryToQueryUserByID(userid)); } QueryUserByIDJob::QueryUserByIDJob(const QString& userid) : BaseJob(HttpVerb::Get, QStringLiteral("QueryUserByIDJob"), - makePath("/_matrix/client/r0", "/thirdparty/user"), + makePath("/_matrix/client/v3", "/thirdparty/user"), queryToQueryUserByID(userid)) {} diff --git a/lib/csapi/third_party_membership.cpp b/lib/csapi/third_party_membership.cpp index 59275e41..3ca986c7 100644 --- a/lib/csapi/third_party_membership.cpp +++ b/lib/csapi/third_party_membership.cpp @@ -10,12 +10,12 @@ InviteBy3PIDJob::InviteBy3PIDJob(const QString& roomId, const QString& idServer, const QString& idAccessToken, const QString& medium, const QString& address) : BaseJob(HttpVerb::Post, QStringLiteral("InviteBy3PIDJob"), - makePath("/_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 1edb969e..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 diff --git a/lib/csapi/to_device.cpp b/lib/csapi/to_device.cpp index 628e8314..e10fac69 100644 --- a/lib/csapi/to_device.cpp +++ b/lib/csapi/to_device.cpp @@ -10,10 +10,10 @@ SendToDeviceJob::SendToDeviceJob( const QString& eventType, const QString& txnId, const QHash<QString, QHash<QString, QJsonObject>>& messages) : BaseJob(HttpVerb::Put, QStringLiteral("SendToDeviceJob"), - makePath("/_matrix/client/r0", "/sendToDevice/", eventType, "/", + makePath("/_matrix/client/v3", "/sendToDevice/", eventType, "/", txnId)) { - QJsonObject _data; - addParam<>(_data, QStringLiteral("messages"), messages); - setRequestData(std::move(_data)); + QJsonObject _dataJson; + addParam<>(_dataJson, QStringLiteral("messages"), messages); + setRequestData({ _dataJson }); } diff --git a/lib/csapi/typing.cpp b/lib/csapi/typing.cpp index c9673118..21bd45ae 100644 --- a/lib/csapi/typing.cpp +++ b/lib/csapi/typing.cpp @@ -9,11 +9,11 @@ using namespace Quotient; SetTypingJob::SetTypingJob(const QString& userId, const QString& roomId, bool typing, Omittable<int> timeout) : BaseJob(HttpVerb::Put, QStringLiteral("SetTypingJob"), - makePath("/_matrix/client/r0", "/rooms/", roomId, "/typing/", + 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/users.cpp b/lib/csapi/users.cpp index 48b727f0..c65280ee 100644 --- a/lib/csapi/users.cpp +++ b/lib/csapi/users.cpp @@ -9,12 +9,12 @@ using namespace Quotient; SearchUserDirectoryJob::SearchUserDirectoryJob(const QString& searchTerm, Omittable<int> limit) : BaseJob(HttpVerb::Post, QStringLiteral("SearchUserDirectoryJob"), - makePath("/_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/versions.h b/lib/csapi/versions.h index 4445dbd2..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 diff --git a/lib/csapi/voip.cpp b/lib/csapi/voip.cpp index c748ad94..1e1f2441 100644 --- a/lib/csapi/voip.cpp +++ b/lib/csapi/voip.cpp @@ -9,10 +9,10 @@ using namespace Quotient; QUrl GetTurnServerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/voip/turnServer")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/voip/turnServer")); } GetTurnServerJob::GetTurnServerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTurnServerJob"), - makePath("/_matrix/client/r0", "/voip/turnServer")) + makePath("/_matrix/client/v3", "/voip/turnServer")) {} diff --git a/lib/csapi/whoami.cpp b/lib/csapi/whoami.cpp index ed8a9817..af0c5d31 100644 --- a/lib/csapi/whoami.cpp +++ b/lib/csapi/whoami.cpp @@ -9,12 +9,12 @@ using namespace Quotient; QUrl GetTokenOwnerJob::makeRequestUrl(QUrl baseUrl) { return BaseJob::makeRequestUrl( - std::move(baseUrl), makePath("/_matrix/client/r0", "/account/whoami")); + std::move(baseUrl), makePath("/_matrix/client/v3", "/account/whoami")); } GetTokenOwnerJob::GetTokenOwnerJob() : BaseJob(HttpVerb::Get, QStringLiteral("GetTokenOwnerJob"), - makePath("/_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 fba099f6..3451dbc3 100644 --- a/lib/csapi/whoami.h +++ b/lib/csapi/whoami.h @@ -41,6 +41,14 @@ public: /// 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 index a85d96bb..79793b9d 100644 --- a/lib/database.cpp +++ b/lib/database.cpp @@ -14,9 +14,7 @@ #include "e2ee/e2ee.h" #include "e2ee/qolmsession.h" #include "e2ee/qolminboundsession.h" -#include "connection.h" -#include "user.h" -#include "room.h" +#include "e2ee/qolmoutboundsession.h" using namespace Quotient; Database::Database(const QString& matrixId, const QString& deviceId, QObject* parent) @@ -31,11 +29,11 @@ Database::Database(const QString& matrixId, const QString& deviceId, QObject* pa database().open(); switch(version()) { - case 0: migrateTo1(); - case 1: migrateTo2(); - case 2: migrateTo3(); - case 3: migrateTo4(); - case 4: migrateTo5(); + case 0: migrateTo1(); [[fallthrough]]; + case 1: migrateTo2(); [[fallthrough]]; + case 2: migrateTo3(); [[fallthrough]]; + case 3: migrateTo4(); [[fallthrough]]; + case 4: migrateTo5(); } } @@ -43,7 +41,7 @@ int Database::version() { auto query = execute(QStringLiteral("PRAGMA user_version;")); if (query.next()) { - bool ok; + bool ok = false; int value = query.value(0).toInt(&ok); qCDebug(DATABASE) << "Database version" << value; if (ok) @@ -212,12 +210,14 @@ UnorderedMap<QString, std::vector<QOlmSessionPtr>> Database::loadOlmSessions(con commit(); UnorderedMap<QString, std::vector<QOlmSessionPtr>> sessions; while (query.next()) { - auto session = QOlmSession::unpickle(query.value("pickle").toByteArray(), picklingMode); - if (std::holds_alternative<QOlmError>(session)) { - qCWarning(E2EE) << "Failed to unpickle olm session"; - continue; - } - sessions[query.value("senderKey").toString()].push_back(std::move(std::get<QOlmSessionPtr>(session))); + 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; } @@ -231,15 +231,15 @@ UnorderedMap<QString, QOlmInboundGroupSessionPtr> Database::loadMegolmSessions(c commit(); UnorderedMap<QString, QOlmInboundGroupSessionPtr> sessions; while (query.next()) { - auto session = QOlmInboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); - if (std::holds_alternative<QOlmError>(session)) { - qCWarning(E2EE) << "Failed to unpickle megolm session"; - continue; - } - - sessions[query.value("sessionId").toString()] = std::move(std::get<QOlmInboundGroupSessionPtr>(session)); - sessions[query.value("sessionId").toString()]->setOlmSessionId(query.value("olmSessionId").toString()); - sessions[query.value("sessionId").toString()]->setSenderId(query.value("senderId").toString()); + 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; } @@ -319,20 +319,22 @@ void Database::setOlmSessionLastReceived(const QString& sessionId, const QDateTi commit(); } -void Database::saveCurrentOutboundMegolmSession(const QString& roomId, const PicklingMode& picklingMode, const QOlmOutboundGroupSessionPtr& session) +void Database::saveCurrentOutboundMegolmSession( + const QString& roomId, const PicklingMode& picklingMode, + const QOlmOutboundGroupSession& session) { - const auto pickle = session->pickle(picklingMode); - if (std::holds_alternative<QByteArray>(pickle)) { + const auto pickle = session.pickle(picklingMode); + if (pickle) { auto deleteQuery = prepareQuery(QStringLiteral("DELETE FROM outbound_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId;")); deleteQuery.bindValue(":roomId", roomId); - deleteQuery.bindValue(":sessionId", session->sessionId()); + 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", std::get<QByteArray>(pickle)); - insertQuery.bindValue(":creationTime", session->creationTime()); - insertQuery.bindValue(":messageCount", session->messageCount()); + insertQuery.bindValue(":sessionId", session.sessionId()); + insertQuery.bindValue(":pickle", pickle.value()); + insertQuery.bindValue(":creationTime", session.creationTime()); + insertQuery.bindValue(":messageCount", session.messageCount()); transaction(); execute(deleteQuery); @@ -348,8 +350,8 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt execute(query); if (query.next()) { auto sessionResult = QOlmOutboundGroupSession::unpickle(query.value("pickle").toByteArray(), picklingMode); - if (std::holds_alternative<QOlmOutboundGroupSessionPtr>(sessionResult)) { - auto session = std::move(std::get<QOlmOutboundGroupSessionPtr>(sessionResult)); + if (sessionResult) { + auto session = std::move(*sessionResult); session->setCreationTime(query.value("creationTime").toDateTime()); session->setMessageCount(query.value("messageCount").toInt()); return session; @@ -358,41 +360,35 @@ QOlmOutboundGroupSessionPtr Database::loadCurrentOutboundMegolmSession(const QSt return nullptr; } -void Database::setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> devices, const QString& sessionId, int index) +void Database::setDevicesReceivedKey(const QString& roomId, const QVector<std::tuple<QString, QString, QString>>& devices, const QString& sessionId, int index) { - auto connection = dynamic_cast<Connection *>(parent()); transaction(); - for (const auto& user : devices.keys()) { - for (const auto& device : devices[user]) { - 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->id()); - query.bindValue(":deviceId", device); - query.bindValue(":identityKey", connection->curveKeyForUserDevice(user->id(), device)); - query.bindValue(":sessionId", sessionId); - query.bindValue(":i", index); - execute(query); - } + 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(); } -QHash<QString, QStringList> Database::devicesWithoutKey(Room* room, const QString &sessionId) +QMultiHash<QString, QString> Database::devicesWithoutKey( + const QString& roomId, QMultiHash<QString, QString> devices, + const QString& sessionId) { - auto connection = dynamic_cast<Connection *>(parent()); - QHash<QString, QStringList> devices; - for (const auto& user : room->users()) { - devices[user->id()] = connection->devicesForUser(user); - } - auto query = prepareQuery(QStringLiteral("SELECT userId, deviceId FROM sent_megolm_sessions WHERE roomId=:roomId AND sessionId=:sessionId")); - query.bindValue(":roomId", room->id()); + query.bindValue(":roomId", roomId); query.bindValue(":sessionId", sessionId); transaction(); execute(query); commit(); while (query.next()) { - devices[query.value("userId").toString()].removeAll(query.value("deviceId").toString()); + devices.remove(query.value("userId").toString(), + query.value("deviceId").toString()); } return devices; } diff --git a/lib/database.h b/lib/database.h index afc41e42..8a133f8e 100644 --- a/lib/database.h +++ b/lib/database.h @@ -11,11 +11,7 @@ #include "e2ee/e2ee.h" -#include "e2ee/qolmoutboundsession.h" - namespace Quotient { -class User; -class Room; class QUOTIENT_API Database : public QObject { @@ -34,21 +30,41 @@ public: 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 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 QOlmOutboundGroupSessionPtr& data); - void updateOlmSession(const QString& senderKey, const QString& sessionId, const QByteArray& pickle); + 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 User -> [Device] that have not received key yet - QHash<QString, QStringList> devicesWithoutKey(Room* room, const QString &sessionId); - void setDevicesReceivedKey(const QString& roomId, QHash<User *, QStringList> devices, const QString& sessionId, int index); + // 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); @@ -62,4 +78,4 @@ private: QString m_matrixId; }; -} +} // namespace Quotient diff --git a/lib/e2ee/e2ee.h b/lib/e2ee/e2ee.h index 268cb525..0772b70a 100644 --- a/lib/e2ee/e2ee.h +++ b/lib/e2ee/e2ee.h @@ -6,9 +6,13 @@ #pragma once #include "converters.h" -#include "quotient_common.h" +#include "expected.h" +#include "qolmerrors.h" #include <QtCore/QMetaType> +#include <QtCore/QStringBuilder> + +#include <array> #include <variant> namespace Quotient { @@ -33,10 +37,11 @@ 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) { - static constexpr auto SupportedAlgorithms = - make_array(OlmV1Curve25519AesSha2AlgoKey, MegolmV1AesSha2AlgoKey); return std::find(SupportedAlgorithms.cbegin(), SupportedAlgorithms.cend(), algorithm) != SupportedAlgorithms.cend(); @@ -55,6 +60,12 @@ 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, QOlmError>; + struct IdentityKeys { QByteArray curve25519; @@ -62,45 +73,66 @@ struct IdentityKeys }; //! Struct representing the one-time keys. -struct QUOTIENT_API OneTimeKeys +struct UnsignedOneTimeKeys { QHash<QString, QHash<QString, QString>> keys; //! Get the HashMap containing the curve25519 one-time keys. - QHash<QString, QString> curve25519() const; - - //! Get a reference to the hashmap corresponding to given key type. -// std::optional<QHash<QString, QString>> get(QString keyType) const; + QHash<QString, QString> curve25519() const { return keys[Curve25519Key]; } }; -//! Struct representing the signed one-time keys. -class SignedOneTimeKey -{ +class SignedOneTimeKey { public: - //! Required. The unpadded Base64-encoded 32-byte Curve25519 public key. - QString key; + 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) + {} - //! Required. Signatures of the key object. - //! The signature is calculated using the process described at Signing JSON. - QHash<QString, QHash<QString, QString>> signatures; -}; + //! Unpadded Base64-encoded 32-byte Curve25519 public key + QString key() const { return payload["key"_ls].toString(); } + //! \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]); + } -template <> -struct JsonObjectConverter<SignedOneTimeKey> { - static void fillFrom(const QJsonObject& jo, SignedOneTimeKey& result) + QByteArray signature(QStringView userId, QStringView deviceId) const { - fromJson(jo.value("key"_ls), result.key); - fromJson(jo.value("signatures"_ls), result.signatures); + return payload["signatures"_ls][userId]["ed25519:"_ls % deviceId] + .toString() + .toLatin1(); } - static void dumpTo(QJsonObject &jo, const SignedOneTimeKey &result) + //! Whether the key is a fallback key + bool isFallback() const { return payload["fallback"_ls].toBool(); } + auto toJson() const { return payload; } + auto toJsonForVerification() const { - addParam<>(jo, QStringLiteral("key"), result.key); - addParam<>(jo, QStringLiteral("signatures"), result.signatures); + 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>>; + template <typename T> class asKeyValueRange { diff --git a/lib/e2ee/qolmaccount.cpp b/lib/e2ee/qolmaccount.cpp index 476a60bd..ccb191f4 100644 --- a/lib/e2ee/qolmaccount.cpp +++ b/lib/e2ee/qolmaccount.cpp @@ -5,6 +5,7 @@ #include "qolmaccount.h" #include "connection.h" +#include "e2ee/qolmsession.h" #include "e2ee/qolmutility.h" #include "e2ee/qolmutils.h" @@ -12,20 +13,9 @@ #include <QtCore/QRandomGenerator> -using namespace Quotient; - -QHash<QString, QString> OneTimeKeys::curve25519() const -{ - return keys[Curve25519Key]; -} +#include <olm/olm.h> -//std::optional<QHash<QString, QString>> OneTimeKeys::get(QString keyType) const -//{ -// if (!keys.contains(keyType)) { -// return std::nullopt; -// } -// return keys[keyType]; -//} +using namespace Quotient; // Convert olm error to enum QOlmError lastError(OlmAccount *account) { @@ -70,7 +60,7 @@ void QOlmAccount::unpickle(QByteArray &pickled, const PicklingMode &mode) } } -std::variant<QByteArray, QOlmError> QOlmAccount::pickle(const PicklingMode &mode) +QOlmExpected<QByteArray> QOlmAccount::pickle(const PicklingMode &mode) { const QByteArray key = toKey(mode); const size_t pickleLength = olm_pickle_account_length(m_account); @@ -119,20 +109,15 @@ QByteArray QOlmAccount::sign(const QJsonObject &message) const QByteArray QOlmAccount::signIdentityKeys() const { const auto keys = identityKeys(); - QJsonObject body - { - {"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)} - } - } - }; - return sign(QJsonDocument(body).toJson(QJsonDocument::Compact)); - + 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 @@ -140,11 +125,15 @@ size_t QOlmAccount::maxNumberOfOneTimeKeys() const return olm_account_max_number_of_one_time_keys(m_account); } -size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const +size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) { - const size_t randomLength = olm_account_generate_one_time_keys_random_length(m_account, numberOfKeys); + const size_t randomLength = + olm_account_generate_one_time_keys_random_length(m_account, + numberOfKeys); QByteArray randomBuffer = getRandom(randomLength); - const auto error = olm_account_generate_one_time_keys(m_account, numberOfKeys, randomBuffer.data(), randomLength); + const auto error = + olm_account_generate_one_time_keys(m_account, numberOfKeys, + randomBuffer.data(), randomLength); if (error == olm_error()) { throw lastError(m_account); @@ -153,49 +142,39 @@ size_t QOlmAccount::generateOneTimeKeys(size_t numberOfKeys) const return error; } -OneTimeKeys QOlmAccount::oneTimeKeys() const +UnsignedOneTimeKeys QOlmAccount::oneTimeKeys() const { const size_t oneTimeKeyLength = olm_account_one_time_keys_length(m_account); - QByteArray oneTimeKeysBuffer(oneTimeKeyLength, '0'); + QByteArray oneTimeKeysBuffer(static_cast<int>(oneTimeKeyLength), '0'); - const auto error = olm_account_one_time_keys(m_account, oneTimeKeysBuffer.data(), oneTimeKeyLength); + const auto error = olm_account_one_time_keys(m_account, + oneTimeKeysBuffer.data(), + oneTimeKeyLength); if (error == olm_error()) { throw lastError(m_account); } const auto json = QJsonDocument::fromJson(oneTimeKeysBuffer).object(); - OneTimeKeys oneTimeKeys; + UnsignedOneTimeKeys oneTimeKeys; fromJson(json, oneTimeKeys.keys); return oneTimeKeys; } -QHash<QString, SignedOneTimeKey> QOlmAccount::signOneTimeKeys(const OneTimeKeys &keys) const +OneTimeKeys QOlmAccount::signOneTimeKeys(const UnsignedOneTimeKeys &keys) const { - QHash<QString, SignedOneTimeKey> signedOneTimeKeys; - for (const auto &keyid : keys.curve25519().keys()) { - const auto oneTimeKey = keys.curve25519()[keyid]; - QByteArray sign = signOneTimeKey(oneTimeKey); - signedOneTimeKeys["signed_curve25519:" + keyid] = signedOneTimeKey(oneTimeKey.toUtf8(), sign); - } + 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; } -SignedOneTimeKey QOlmAccount::signedOneTimeKey(const QByteArray &key, const QString &signature) const -{ - SignedOneTimeKey sign{}; - sign.key = key; - sign.signatures = {{m_userId, {{"ed25519:" + m_deviceId, signature}}}}; - return sign; -} - -QByteArray QOlmAccount::signOneTimeKey(const QString &key) const +std::optional<QOlmError> QOlmAccount::removeOneTimeKeys( + const QOlmSession& session) { - QJsonDocument j(QJsonObject{{"key", key}}); - return sign(j.toJson(QJsonDocument::Compact)); -} - -std::optional<QOlmError> QOlmAccount::removeOneTimeKeys(const QOlmSessionPtr &session) const -{ - const auto error = olm_remove_one_time_keys(m_account, session->raw()); + const auto error = olm_remove_one_time_keys(m_account, session.raw()); if (error == olm_error()) { return lastError(m_account); @@ -208,54 +187,47 @@ OlmAccount* QOlmAccount::data() { return m_account; } DeviceKeys QOlmAccount::deviceKeys() const { - DeviceKeys deviceKeys; - deviceKeys.userId = m_userId; - deviceKeys.deviceId = m_deviceId; - deviceKeys.algorithms = QStringList {"m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"}; + static QStringList Algorithms(SupportedAlgorithms.cbegin(), + SupportedAlgorithms.cend()); const auto idKeys = identityKeys(); - deviceKeys.keys["curve25519:" + m_deviceId] = idKeys.curve25519; - deviceKeys.keys["ed25519:" + m_deviceId] = idKeys.ed25519; - - const auto sign = signIdentityKeys(); - deviceKeys.signatures[m_userId]["ed25519:" + m_deviceId] = sign; - - return deviceKeys; + 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 OneTimeKeys &oneTimeKeys) +UploadKeysJob* QOlmAccount::createUploadKeyRequest( + const UnsignedOneTimeKeys& oneTimeKeys) const { - auto keys = deviceKeys(); - - if (oneTimeKeys.curve25519().isEmpty()) { - return new UploadKeysJob(keys); - } - - // Sign & append the one time keys. - auto temp = signOneTimeKeys(oneTimeKeys); - QHash<QString, QVariant> oneTimeKeysSigned; - for (const auto &[keyId, key] : asKeyValueRange(temp)) { - oneTimeKeysSigned[keyId] = QVariant::fromValue(toJson(key)); - } - - return new UploadKeysJob(keys, oneTimeKeysSigned); + return new UploadKeysJob(deviceKeys(), signOneTimeKeys(oneTimeKeys)); } -std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSession(const QOlmMessage &preKeyMessage) +QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSession( + const QOlmMessage& preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); return QOlmSession::createInboundSession(this, preKeyMessage); } -std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage) +QOlmExpected<QOlmSessionPtr> QOlmAccount::createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage) { Q_ASSERT(preKeyMessage.type() == QOlmMessage::PreKey); - return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, preKeyMessage); + return QOlmSession::createInboundSessionFrom(this, theirIdentityKey, + preKeyMessage); } -std::variant<QOlmSessionPtr, QOlmError> QOlmAccount::createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey) +QOlmExpected<QOlmSessionPtr> QOlmAccount::createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey) { - return QOlmSession::createOutboundSession(this, theirIdentityKey, theirOneTimeKey); + return QOlmSession::createOutboundSession(this, theirIdentityKey, + theirOneTimeKey); } void QOlmAccount::markKeysAsPublished() @@ -292,10 +264,6 @@ bool Quotient::ed25519VerifySignature(const QString& signingKey, QByteArray signingKeyBuf = signingKey.toUtf8(); QOlmUtility utility; auto signatureBuf = signature.toUtf8(); - auto result = utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf); - if (std::holds_alternative<QOlmError>(result)) { - return false; - } - - return std::get<bool>(result); + return utility.ed25519Verify(signingKeyBuf, canonicalJson, signatureBuf) + .value_or(false); } diff --git a/lib/e2ee/qolmaccount.h b/lib/e2ee/qolmaccount.h index 17f43f1a..f2a31314 100644 --- a/lib/e2ee/qolmaccount.h +++ b/lib/e2ee/qolmaccount.h @@ -5,21 +5,16 @@ #pragma once -#include "csapi/keys.h" #include "e2ee/e2ee.h" -#include "e2ee/qolmerrors.h" #include "e2ee/qolmmessage.h" -#include "e2ee/qolmsession.h" -#include <QObject> -struct OlmAccount; +#include "csapi/keys.h" -namespace Quotient { +#include <QtCore/QObject> -class QOlmSession; -class Connection; +struct OlmAccount; -using QOlmSessionPtr = std::unique_ptr<QOlmSession>; +namespace Quotient { //! An olm account manages all cryptographic keys used on a device. //! \code{.cpp} @@ -30,7 +25,7 @@ class QUOTIENT_API QOlmAccount : public QObject Q_OBJECT public: QOlmAccount(const QString &userId, const QString &deviceId, QObject *parent = nullptr); - ~QOlmAccount(); + ~QOlmAccount() override; //! Creates a new instance of OlmAccount. During the instantiation //! the Ed25519 fingerprint key pair and the Curve25519 identity key @@ -44,7 +39,7 @@ public: void unpickle(QByteArray &pickled, const PicklingMode &mode); //! Serialises an OlmAccount to encrypted Base64. - std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode); + QOlmExpected<QByteArray> pickle(const PicklingMode &mode); //! Returns the account's public identity keys already formatted as JSON IdentityKeys identityKeys() const; @@ -61,40 +56,39 @@ public: size_t maxNumberOfOneTimeKeys() const; //! Generates the supplied number of one time keys. - size_t generateOneTimeKeys(size_t numberOfKeys) const; + size_t generateOneTimeKeys(size_t numberOfKeys); //! Gets the OlmAccount's one time keys formatted as JSON. - OneTimeKeys oneTimeKeys() const; + UnsignedOneTimeKeys oneTimeKeys() const; //! Sign all one time keys. - QHash<QString, SignedOneTimeKey> signOneTimeKeys(const OneTimeKeys &keys) const; - - //! Sign one time key. - QByteArray signOneTimeKey(const QString &key) const; - - SignedOneTimeKey signedOneTimeKey(const QByteArray &key, const QString &signature) const; + OneTimeKeys signOneTimeKeys(const UnsignedOneTimeKeys &keys) const; - UploadKeysJob *createUploadKeyRequest(const OneTimeKeys &oneTimeKeys); + UploadKeysJob* createUploadKeyRequest(const UnsignedOneTimeKeys& oneTimeKeys) const; DeviceKeys deviceKeys() const; //! Remove the one time key used to create the supplied session. - [[nodiscard]] std::optional<QOlmError> removeOneTimeKeys(const QOlmSessionPtr &session) const; + [[nodiscard]] std::optional<QOlmError> removeOneTimeKeys( + const QOlmSession& session); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. //! //! \param message An Olm pre-key message that was encrypted for this account. - std::variant<QOlmSessionPtr, QOlmError> createInboundSession(const QOlmMessage &preKeyMessage); + 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. - std::variant<QOlmSessionPtr, QOlmError> createInboundSessionFrom(const QByteArray &theirIdentityKey, const QOlmMessage &preKeyMessage); + QOlmExpected<QOlmSessionPtr> createInboundSessionFrom( + const QByteArray& theirIdentityKey, const QOlmMessage& preKeyMessage); //! Creates an outbound session for sending messages to a specific /// identity and one time key. - std::variant<QOlmSessionPtr, QOlmError> createOutboundSession(const QByteArray &theirIdentityKey, const QByteArray &theirOneTimeKey); + QOlmExpected<QOlmSessionPtr> createOutboundSession( + const QByteArray& theirIdentityKey, const QByteArray& theirOneTimeKey); void markKeysAsPublished(); @@ -103,7 +97,7 @@ public: OlmAccount *data(); Q_SIGNALS: - void needsSave() const; + void needsSave(); private: OlmAccount *m_account = nullptr; // owning diff --git a/lib/e2ee/qolminboundsession.cpp b/lib/e2ee/qolminboundsession.cpp index 60d871ef..17f06205 100644 --- a/lib/e2ee/qolminboundsession.cpp +++ b/lib/e2ee/qolminboundsession.cpp @@ -70,7 +70,8 @@ QByteArray QOlmInboundGroupSession::pickle(const PicklingMode &mode) const return pickledBuf; } -std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> QOlmInboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +QOlmExpected<QOlmInboundGroupSessionPtr> QOlmInboundGroupSession::unpickle( + const QByteArray& pickled, const PicklingMode& mode) { QByteArray pickledBuf = pickled; const auto groupSession = olm_inbound_group_session(new uint8_t[olm_inbound_group_session_size()]); @@ -85,7 +86,8 @@ std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> QOlmInboundGro return std::make_unique<QOlmInboundGroupSession>(groupSession); } -std::variant<std::pair<QString, uint32_t>, QOlmError> QOlmInboundGroupSession::decrypt(const QByteArray &message) +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; @@ -114,10 +116,10 @@ std::variant<std::pair<QString, uint32_t>, QOlmError> QOlmInboundGroupSession::d QByteArray output(plaintextLen, '0'); std::memcpy(output.data(), plaintextBuf.data(), plaintextLen); - return std::make_pair<QString, qint32>(QString(output), messageIndex); + return std::make_pair(output, messageIndex); } -std::variant<QByteArray, QOlmError> QOlmInboundGroupSession::exportSession(uint32_t messageIndex) +QOlmExpected<QByteArray> QOlmInboundGroupSession::exportSession(uint32_t messageIndex) { const auto keyLength = olm_export_inbound_group_session_length(m_groupSession); QByteArray keyBuf(keyLength, '0'); @@ -154,9 +156,9 @@ QString QOlmInboundGroupSession::olmSessionId() const { return m_olmSessionId; } -void QOlmInboundGroupSession::setOlmSessionId(const QString& olmSessionId) +void QOlmInboundGroupSession::setOlmSessionId(const QString& newOlmSessionId) { - m_olmSessionId = olmSessionId; + m_olmSessionId = newOlmSessionId; } QString QOlmInboundGroupSession::senderId() const diff --git a/lib/e2ee/qolminboundsession.h b/lib/e2ee/qolminboundsession.h index 32112b97..1a9b4415 100644 --- a/lib/e2ee/qolminboundsession.h +++ b/lib/e2ee/qolminboundsession.h @@ -5,11 +5,8 @@ #pragma once #include "e2ee/e2ee.h" -#include "e2ee/qolmerrors.h" -#include "olm/olm.h" -#include <memory> -#include <variant> +#include <olm/olm.h> namespace Quotient { @@ -27,14 +24,13 @@ public: QByteArray pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling //! an `OlmInboundGroupSession`. - static std::variant<std::unique_ptr<QOlmInboundGroupSession>, QOlmError> - unpickle(const QByteArray& picked, const PicklingMode& mode); + static QOlmExpected<QOlmInboundGroupSessionPtr> unpickle( + const QByteArray& pickled, const PicklingMode& mode); //! Decrypts ciphertext received for this group session. - std::variant<std::pair<QString, uint32_t>, QOlmError> decrypt( - const QByteArray& message); + 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. - std::variant<QByteArray, QOlmError> exportSession(uint32_t messageIndex); + 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. @@ -44,7 +40,7 @@ public: //! 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& setOlmSessionId); + void setOlmSessionId(const QString& newOlmSessionId); //! The sender of this session. QString senderId() const; diff --git a/lib/e2ee/qolmmessage.cpp b/lib/e2ee/qolmmessage.cpp index 81b166b0..f9b4a5c2 100644 --- a/lib/e2ee/qolmmessage.cpp +++ b/lib/e2ee/qolmmessage.cpp @@ -4,6 +4,8 @@ #include "qolmmessage.h" +#include "util.h" + using namespace Quotient; QOlmMessage::QOlmMessage(QByteArray ciphertext, QOlmMessage::Type type) @@ -26,7 +28,7 @@ QOlmMessage::Type QOlmMessage::type() const QByteArray QOlmMessage::toCiphertext() const { - return QByteArray(*this); + return SLICE(*this, QByteArray); } QOlmMessage QOlmMessage::fromCiphertext(const QByteArray &ciphertext) diff --git a/lib/e2ee/qolmoutboundsession.cpp b/lib/e2ee/qolmoutboundsession.cpp index 8852bcf3..a2eff2c8 100644 --- a/lib/e2ee/qolmoutboundsession.cpp +++ b/lib/e2ee/qolmoutboundsession.cpp @@ -13,8 +13,7 @@ QOlmError lastError(OlmOutboundGroupSession *session) { QOlmOutboundGroupSession::QOlmOutboundGroupSession(OlmOutboundGroupSession *session) : m_groupSession(session) -{ -} +{} QOlmOutboundGroupSession::~QOlmOutboundGroupSession() { @@ -22,7 +21,7 @@ QOlmOutboundGroupSession::~QOlmOutboundGroupSession() delete[](reinterpret_cast<uint8_t *>(m_groupSession)); } -std::unique_ptr<QOlmOutboundGroupSession> QOlmOutboundGroupSession::create() +QOlmOutboundGroupSessionPtr QOlmOutboundGroupSession::create() { auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); const auto randomLength = olm_init_outbound_group_session_random_length(olmOutboundGroupSession); @@ -45,7 +44,7 @@ std::unique_ptr<QOlmOutboundGroupSession> QOlmOutboundGroupSession::create() return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); } -std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const PicklingMode &mode) +QOlmExpected<QByteArray> QOlmOutboundGroupSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_outbound_group_session_length(m_groupSession), '0'); QByteArray key = toKey(mode); @@ -61,7 +60,7 @@ std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::pickle(const Pickl return pickledBuf; } -std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +QOlmExpected<QOlmOutboundGroupSessionPtr> QOlmOutboundGroupSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) { QByteArray pickledBuf = pickled; auto *olmOutboundGroupSession = olm_outbound_group_session(new uint8_t[olm_outbound_group_session_size()]); @@ -80,7 +79,7 @@ std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> QOlmOutboundG return std::make_unique<QOlmOutboundGroupSession>(olmOutboundGroupSession); } -std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::encrypt(const QString &plaintext) +QOlmExpected<QByteArray> QOlmOutboundGroupSession::encrypt(const QString &plaintext) const { QByteArray plaintextBuf = plaintext.toUtf8(); const auto messageMaxLength = olm_group_encrypt_message_length(m_groupSession, plaintextBuf.length()); @@ -112,12 +111,13 @@ QByteArray QOlmOutboundGroupSession::sessionId() const return idBuffer; } -std::variant<QByteArray, QOlmError> QOlmOutboundGroupSession::sessionKey() const +QOlmExpected<QByteArray> QOlmOutboundGroupSession::sessionKey() const { const auto keyMaxLength = olm_outbound_group_session_key_length(m_groupSession); QByteArray keyBuffer(keyMaxLength, '0'); - const auto error = olm_outbound_group_session_key(m_groupSession, reinterpret_cast<uint8_t *>(keyBuffer.data()), - keyMaxLength); + const auto error = olm_outbound_group_session_key( + m_groupSession, reinterpret_cast<uint8_t*>(keyBuffer.data()), + keyMaxLength); if (error == olm_error()) { return lastError(m_groupSession); } diff --git a/lib/e2ee/qolmoutboundsession.h b/lib/e2ee/qolmoutboundsession.h index 10ca35c0..9a82d22a 100644 --- a/lib/e2ee/qolmoutboundsession.h +++ b/lib/e2ee/qolmoutboundsession.h @@ -4,10 +4,10 @@ #pragma once -#include "olm/olm.h" -#include "e2ee/qolmerrors.h" #include "e2ee/e2ee.h" + #include <memory> +#include <olm/olm.h> namespace Quotient { @@ -19,15 +19,16 @@ public: ~QOlmOutboundGroupSession(); //! Creates a new instance of `QOlmOutboundGroupSession`. //! Throw OlmError on errors - static std::unique_ptr<QOlmOutboundGroupSession> create(); + static QOlmOutboundGroupSessionPtr create(); //! Serialises a `QOlmOutboundGroupSession` to encrypted Base64. - std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode); + QOlmExpected<QByteArray> pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by //! pickling a `QOlmOutboundGroupSession`. - static std::variant<std::unique_ptr<QOlmOutboundGroupSession>, QOlmError> - unpickle(const QByteArray& pickled, const PicklingMode& mode); + static QOlmExpected<QOlmOutboundGroupSessionPtr> unpickle( + const QByteArray& pickled, const PicklingMode& mode); + //! Encrypts a plaintext message using the session. - std::variant<QByteArray, QOlmError> encrypt(const QString &plaintext); + QOlmExpected<QByteArray> encrypt(const QString& plaintext) const; //! Get the current message index for this session. //! @@ -42,7 +43,7 @@ public: //! //! Each message is sent with a different ratchet key. This function returns the //! ratchet key that will be used for the next message. - std::variant<QByteArray, QOlmError> sessionKey() const; + QOlmExpected<QByteArray> sessionKey() const; QOlmOutboundGroupSession(OlmOutboundGroupSession *groupSession); int messageCount() const; @@ -56,5 +57,4 @@ private: QDateTime m_creationTime = QDateTime::currentDateTime(); }; -using QOlmOutboundGroupSessionPtr = std::unique_ptr<QOlmOutboundGroupSession>; -} +} // namespace Quotient diff --git a/lib/e2ee/qolmsession.cpp b/lib/e2ee/qolmsession.cpp index e575ff39..2a98d5d8 100644 --- a/lib/e2ee/qolmsession.cpp +++ b/lib/e2ee/qolmsession.cpp @@ -3,10 +3,12 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "qolmsession.h" + #include "e2ee/qolmutils.h" #include "logging.h" + #include <cstring> -#include <QDebug> +#include <olm/olm.h> using namespace Quotient; @@ -25,7 +27,9 @@ OlmSession* QOlmSession::create() return olm_session(new uint8_t[olm_session_size()]); } -std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInbound(QOlmAccount *account, const QOlmMessage &preKeyMessage, bool from, const QString &theirIdentityKey) +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 in when creating inbound session" << BadMessageFormat; @@ -51,17 +55,22 @@ std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInbound(QOlmAccount * return std::make_unique<QOlmSession>(olmSession); } -std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSession(QOlmAccount *account, const QOlmMessage &preKeyMessage) +QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage) { return createInbound(account, preKeyMessage); } -std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createInboundSessionFrom(QOlmAccount *account, const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) +QOlmExpected<QOlmSessionPtr> QOlmSession::createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage) { return createInbound(account, preKeyMessage, true, theirIdentityKey); } -std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createOutboundSession(QOlmAccount *account, const QString &theirIdentityKey, const QString &theirOneTimeKey) +QOlmExpected<QOlmSessionPtr> QOlmSession::createOutboundSession( + QOlmAccount* account, const QString& theirIdentityKey, + const QString& theirOneTimeKey) { auto *olmOutboundSession = create(); const auto randomLen = olm_create_outbound_session_random_length(olmOutboundSession); @@ -87,12 +96,13 @@ std::variant<QOlmSessionPtr, QOlmError> QOlmSession::createOutboundSession(QOlmA return std::make_unique<QOlmSession>(olmOutboundSession); } -std::variant<QByteArray, QOlmError> QOlmSession::pickle(const PicklingMode &mode) +QOlmExpected<QByteArray> QOlmSession::pickle(const PicklingMode &mode) const { QByteArray pickledBuf(olm_pickle_session_length(m_session), '0'); QByteArray key = toKey(mode); const auto error = olm_pickle_session(m_session, key.data(), key.length(), - pickledBuf.data(), pickledBuf.length()); + pickledBuf.data(), + pickledBuf.length()); if (error == olm_error()) { return lastError(m_session); @@ -103,7 +113,8 @@ std::variant<QByteArray, QOlmError> QOlmSession::pickle(const PicklingMode &mode return pickledBuf; } -std::variant<QOlmSessionPtr, QOlmError> QOlmSession::unpickle(const QByteArray &pickled, const PicklingMode &mode) +QOlmExpected<QOlmSessionPtr> QOlmSession::unpickle(const QByteArray& pickled, + const PicklingMode& mode) { QByteArray pickledBuf = pickled; auto *olmSession = create(); @@ -138,7 +149,7 @@ QOlmMessage QOlmSession::encrypt(const QString &plaintext) return QOlmMessage(messageBuf, messageType); } -std::variant<QString, QOlmError> QOlmSession::decrypt(const QOlmMessage &message) const +QOlmExpected<QByteArray> QOlmSession::decrypt(const QOlmMessage &message) const { const auto messageType = message.type(); const auto ciphertext = message.toCiphertext(); @@ -207,45 +218,35 @@ bool QOlmSession::hasReceivedMessage() const return olm_session_has_received_message(m_session); } -std::variant<bool, QOlmError> QOlmSession::matchesInboundSession(const QOlmMessage &preKeyMessage) const +bool QOlmSession::matchesInboundSession(const QOlmMessage& preKeyMessage) const { Q_ASSERT(preKeyMessage.type() == QOlmMessage::Type::PreKey); QByteArray oneTimeKeyBuf(preKeyMessage.data()); - const auto matchesResult = olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), oneTimeKeyBuf.length()); + const auto maybeMatches = + olm_matches_inbound_session(m_session, oneTimeKeyBuf.data(), + oneTimeKeyBuf.length()); - if (matchesResult == olm_error()) { + if (maybeMatches == olm_error()) { return lastError(m_session); } - switch (matchesResult) { - case 0: - return false; - case 1: - return true; - default: - return QOlmError::Unknown; - } + return maybeMatches == 1; } -std::variant<bool, QOlmError> QOlmSession::matchesInboundSessionFrom(const QString &theirIdentityKey, const QOlmMessage &preKeyMessage) const + +bool QOlmSession::matchesInboundSessionFrom( + const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const { const auto theirIdentityKeyBuf = theirIdentityKey.toUtf8(); auto oneTimeKeyMessageBuf = preKeyMessage.toCiphertext(); - const auto error = olm_matches_inbound_session_from(m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), - oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); + const auto maybeMatches = olm_matches_inbound_session_from( + m_session, theirIdentityKeyBuf.data(), theirIdentityKeyBuf.length(), + oneTimeKeyMessageBuf.data(), oneTimeKeyMessageBuf.length()); - if (error == olm_error()) { - return lastError(m_session); - } - switch (error) { - case 0: - return false; - case 1: - return true; - default: - return QOlmError::Unknown; - } + if (maybeMatches == olm_error()) + qCWarning(E2EE) << "Error matching an inbound session:" + << olm_session_last_error(m_session); + return maybeMatches == 1; } QOlmSession::QOlmSession(OlmSession *session) : m_session(session) -{ -} +{} diff --git a/lib/e2ee/qolmsession.h b/lib/e2ee/qolmsession.h index f20c9837..021092c7 100644 --- a/lib/e2ee/qolmsession.h +++ b/lib/e2ee/qolmsession.h @@ -4,17 +4,14 @@ #pragma once -#include <QDebug> -#include <olm/olm.h> // FIXME: OlmSession #include "e2ee/e2ee.h" #include "e2ee/qolmmessage.h" #include "e2ee/qolmerrors.h" #include "e2ee/qolmaccount.h" -namespace Quotient { +struct OlmSession; -class QOlmAccount; -class QOlmSession; +namespace Quotient { //! Either an outbound or inbound session for secure communication. class QUOTIENT_API QOlmSession @@ -22,32 +19,31 @@ class QUOTIENT_API QOlmSession public: ~QOlmSession(); //! Creates an inbound session for sending/receiving messages from a received 'prekey' message. - static std::variant<std::unique_ptr<QOlmSession>, QOlmError> - createInboundSession(QOlmAccount* account, const QOlmMessage& preKeyMessage); + static QOlmExpected<QOlmSessionPtr> createInboundSession( + QOlmAccount* account, const QOlmMessage& preKeyMessage); - static std::variant<std::unique_ptr<QOlmSession>, QOlmError> - createInboundSessionFrom(QOlmAccount* account, - const QString& theirIdentityKey, - const QOlmMessage& preKeyMessage); + static QOlmExpected<QOlmSessionPtr> createInboundSessionFrom( + QOlmAccount* account, const QString& theirIdentityKey, + const QOlmMessage& preKeyMessage); - static std::variant<std::unique_ptr<QOlmSession>, QOlmError> - createOutboundSession(QOlmAccount* account, const QString& theirIdentityKey, - const QString& theirOneTimeKey); + static QOlmExpected<QOlmSessionPtr> createOutboundSession( + QOlmAccount* account, const QString& theirIdentityKey, + const QString& theirOneTimeKey); //! Serialises an `QOlmSession` to encrypted Base64. - std::variant<QByteArray, QOlmError> pickle(const PicklingMode &mode); + QOlmExpected<QByteArray> pickle(const PicklingMode &mode) const; //! Deserialises from encrypted Base64 that was previously obtained by pickling a `QOlmSession`. - static std::variant<std::unique_ptr<QOlmSession>, QOlmError> unpickle( + static QOlmExpected<QOlmSessionPtr> unpickle( const QByteArray& pickled, const PicklingMode& mode); //! Encrypts a plaintext message using the session. QOlmMessage encrypt(const QString &plaintext); - //! Decrypts a message using this session. Decoding is lossy, meaing if + //! 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` (�). - std::variant<QString, QOlmError> decrypt(const QOlmMessage &message) const; + QOlmExpected<QByteArray> decrypt(const QOlmMessage &message) const; //! Get a base64-encoded identifier for this session. QByteArray sessionId() const; @@ -59,11 +55,10 @@ public: bool hasReceivedMessage() const; //! Checks if the 'prekey' message is for this in-bound session. - std::variant<bool, QOlmError> matchesInboundSession( - const QOlmMessage& preKeyMessage) const; + bool matchesInboundSession(const QOlmMessage& preKeyMessage) const; //! Checks if the 'prekey' message is for this in-bound session. - std::variant<bool, QOlmError> matchesInboundSessionFrom( + bool matchesInboundSessionFrom( const QString& theirIdentityKey, const QOlmMessage& preKeyMessage) const; friend bool operator<(const QOlmSession& lhs, const QOlmSession& rhs) @@ -71,8 +66,7 @@ public: return lhs.sessionId() < rhs.sessionId(); } - friend bool operator<(const std::unique_ptr<QOlmSession>& lhs, - const std::unique_ptr<QOlmSession>& rhs) + friend bool operator<(const QOlmSessionPtr& lhs, const QOlmSessionPtr& rhs) { return *lhs < *rhs; } @@ -83,7 +77,7 @@ public: private: //! Helper function for creating new sessions and handling errors. static OlmSession* create(); - static std::variant<std::unique_ptr<QOlmSession>, QOlmError> createInbound( + static QOlmExpected<QOlmSessionPtr> createInbound( QOlmAccount* account, const QOlmMessage& preKeyMessage, bool from = false, const QString& theirIdentityKey = ""); OlmSession* m_session; diff --git a/lib/e2ee/qolmutility.cpp b/lib/e2ee/qolmutility.cpp index 9f09a37f..84559085 100644 --- a/lib/e2ee/qolmutility.cpp +++ b/lib/e2ee/qolmutility.cpp @@ -3,8 +3,8 @@ // SPDX-License-Identifier: LGPL-2.1-or-later #include "e2ee/qolmutility.h" -#include "olm/olm.h" -#include <QDebug> + +#include <olm/olm.h> using namespace Quotient; @@ -40,8 +40,9 @@ QString QOlmUtility::sha256Utf8Msg(const QString &message) const return sha256Bytes(message.toUtf8()); } -std::variant<bool, QOlmError> QOlmUtility::ed25519Verify(const QByteArray &key, - const QByteArray &message, const QByteArray &signature) +QOlmExpected<bool> QOlmUtility::ed25519Verify(const QByteArray& key, + const QByteArray& message, + const QByteArray& signature) { QByteArray signatureBuf(signature.length(), '0'); std::copy(signature.begin(), signature.end(), signatureBuf.begin()); @@ -57,8 +58,5 @@ std::variant<bool, QOlmError> QOlmUtility::ed25519Verify(const QByteArray &key, return error; } - if (ret != 0) { - return false; - } - return true; + return !ret; // ret == 0 means success } diff --git a/lib/e2ee/qolmutility.h b/lib/e2ee/qolmutility.h index a12af49a..5f6bcdc5 100644 --- a/lib/e2ee/qolmutility.h +++ b/lib/e2ee/qolmutility.h @@ -4,15 +4,12 @@ #pragma once -#include <variant> -#include "e2ee/qolmerrors.h" +#include "e2ee/e2ee.h" struct OlmUtility; namespace Quotient { -class QOlmSession; - //! Allows you to make use of crytographic hashing via SHA-2 and //! verifying ed25519 signatures. class QUOTIENT_API QOlmUtility @@ -32,7 +29,7 @@ public: //! \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. - std::variant<bool, QOlmError> ed25519Verify(const QByteArray &key, + QOlmExpected<bool> ed25519Verify(const QByteArray &key, const QByteArray &message, const QByteArray &signature); private: diff --git a/lib/eventitem.cpp b/lib/eventitem.cpp index 302ae053..a2e2a156 100644 --- a/lib/eventitem.cpp +++ b/lib/eventitem.cpp @@ -8,32 +8,23 @@ 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; }); - } - setStatus(EventStatus::FileUploaded); -} - -void PendingEventItem::setEncryptedFile(const EncryptedFile& encryptedFile) -{ - if (auto* rme = getAs<RoomMessageEvent>()) { - Q_ASSERT(rme->hasFileContent()); - rme->editContent([encryptedFile](EventContent::TypedBase& ec) { - ec.fileInfo()->file = encryptedFile; + rae->editContent([&uploadedFileData](EventContent::FileInfo& fi) { + fi.source = uploadedFileData; }); } + setStatus(EventStatus::FileUploaded); } // Not exactly sure why but this helps with the linker not finding diff --git a/lib/eventitem.h b/lib/eventitem.h index d8313736..90d9f458 100644 --- a/lib/eventitem.h +++ b/lib/eventitem.h @@ -3,18 +3,18 @@ #pragma once -#include "events/stateevent.h" #include "quotient_common.h" +#include "events/filesourceinfo.h" +#include "events/stateevent.h" + #include <any> #include <utility> -#include "events/encryptedfile.h" - namespace Quotient { namespace EventStatus { - QUO_NAMESPACE + Q_NAMESPACE_EXPORT(QUOTIENT_API) /** Special marks an event can assume * @@ -22,16 +22,16 @@ namespace EventStatus { * 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_ENUM_NS(Code) } // namespace EventStatus @@ -115,8 +115,7 @@ public: QString annotation() const { return _annotation; } void setDeparted() { setStatus(EventStatus::Departed); } - void setFileUploaded(const QUrl& remoteUrl); - void setEncryptedFile(const EncryptedFile& encryptedFile); + void setFileUploaded(const FileSourceInfo &uploadedFileData); void setReachedServer(const QString& eventId) { setStatus(EventStatus::ReachedServer); diff --git a/lib/events/accountdataevents.h b/lib/events/accountdataevents.h index 12f1f00b..24c3353c 100644 --- a/lib/events/accountdataevents.h +++ b/lib/events/accountdataevents.h @@ -32,7 +32,7 @@ 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; @@ -46,27 +46,14 @@ struct JsonObjectConverter<TagRecord> { using TagsMap = QHash<QString, TagRecord>; -#define DEFINE_SIMPLE_EVENT(_Name, _TypeId, _ContentType, _ContentKey) \ - class QUOTIENT_API _Name : public Event { \ - public: \ - using content_type = _ContentType; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - explicit _Name(const QJsonObject& obj) : Event(typeId(), obj) {} \ - explicit _Name(const content_type& content) \ - : Event(typeId(), matrixTypeId(), \ - QJsonObject { \ - { QStringLiteral(#_ContentKey), toJson(content) } }) \ - {} \ - auto _ContentKey() const \ - { \ - return contentPart<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>, +DEFINE_SIMPLE_EVENT(TagEvent, Event, "m.tag", TagsMap, tags) +DEFINE_SIMPLE_EVENT(ReadMarkerEventImpl, Event, "m.fully_read", QString, eventId) +class ReadMarkerEvent : public ReadMarkerEventImpl { +public: + using ReadMarkerEventImpl::ReadMarkerEventImpl; + [[deprecated("Use ReadMarkerEvent::eventId() instead")]] + QString event_id() const { return eventId(); } +}; +DEFINE_SIMPLE_EVENT(IgnoredUsersEvent, Event, "m.ignored_user_list", QSet<QString>, ignored_users) } // namespace Quotient diff --git a/lib/events/callanswerevent.cpp b/lib/events/callanswerevent.cpp index be83d9d0..f75f8ad3 100644 --- a/lib/events/callanswerevent.cpp +++ b/lib/events/callanswerevent.cpp @@ -14,7 +14,6 @@ m.call.answer "type": "answer" }, "call_id": "12345", - "lifetime": 60000, "version": 0 }, "event_id": "$WLGTSEFSEF:localhost", @@ -33,16 +32,6 @@ CallAnswerEvent::CallAnswerEvent(const QJsonObject& 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, diff --git a/lib/events/callanswerevent.h b/lib/events/callanswerevent.h index 8ffe60f2..4d539b85 100644 --- a/lib/events/callanswerevent.h +++ b/lib/events/callanswerevent.h @@ -13,14 +13,8 @@ public: 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 contentPart<int>("lifetime"_ls); - } // FIXME: Omittable<>? QString sdp() const { return contentPart<QJsonObject>("answer"_ls).value("sdp"_ls).toString(); diff --git a/lib/events/callcandidatesevent.h b/lib/events/callcandidatesevent.h index 74c38f2c..e949f722 100644 --- a/lib/events/callcandidatesevent.h +++ b/lib/events/callcandidatesevent.h @@ -23,20 +23,9 @@ public: { { QStringLiteral("candidates"), candidates } }) {} - QJsonArray candidates() const - { - return contentPart<QJsonArray>("candidates"_ls); - } - - QString callId() const - { - return contentPart<QString>("call_id"); - } - - int version() const - { - return contentPart<int>("version"); - } + QUO_CONTENT_GETTER(QJsonArray, candidates) + QUO_CONTENT_GETTER(QString, callId) + QUO_CONTENT_GETTER(int, version) }; REGISTER_EVENT_TYPE(CallCandidatesEvent) diff --git a/lib/events/callinviteevent.cpp b/lib/events/callinviteevent.cpp index 11d50768..2f26a1cb 100644 --- a/lib/events/callinviteevent.cpp +++ b/lib/events/callinviteevent.cpp @@ -33,7 +33,7 @@ CallInviteEvent::CallInviteEvent(const QJsonObject& obj) qCDebug(EVENTS) << "Call Invite event"; } -CallInviteEvent::CallInviteEvent(const QString& callId, const int lifetime, +CallInviteEvent::CallInviteEvent(const QString& callId, int lifetime, const QString& sdp) : CallEventBase( typeId(), matrixTypeId(), callId, 0, diff --git a/lib/events/callinviteevent.h b/lib/events/callinviteevent.h index 47362b5c..5b4ca0df 100644 --- a/lib/events/callinviteevent.h +++ b/lib/events/callinviteevent.h @@ -13,13 +13,10 @@ public: explicit CallInviteEvent(const QJsonObject& obj); - explicit CallInviteEvent(const QString& callId, const int lifetime, + explicit CallInviteEvent(const QString& callId, int lifetime, const QString& sdp); - int lifetime() const - { - return contentPart<int>("lifetime"_ls); - } // FIXME: Omittable<>? + QUO_CONTENT_GETTER(int, lifetime) QString sdp() const { return contentPart<QJsonObject>("offer"_ls).value("sdp"_ls).toString(); diff --git a/lib/events/directchatevent.cpp b/lib/events/directchatevent.cpp index 0ee1f7b0..83bb1e32 100644 --- a/lib/events/directchatevent.cpp +++ b/lib/events/directchatevent.cpp @@ -3,8 +3,6 @@ #include "directchatevent.h" -#include <QtCore/QJsonArray> - using namespace Quotient; QMultiHash<QString, QString> DirectChatEvent::usersToDirectChats() const diff --git a/lib/events/encryptedevent.cpp b/lib/events/encryptedevent.cpp index 3af3d6ff..ec00ad4c 100644 --- a/lib/events/encryptedevent.cpp +++ b/lib/events/encryptedevent.cpp @@ -49,14 +49,16 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const eventObject["event_id"] = id(); eventObject["sender"] = senderId(); eventObject["origin_server_ts"] = originTimestamp().toMSecsSinceEpoch(); - if (const auto relatesToJson = contentPart("m.relates_to"_ls); !relatesToJson.isUndefined()) { + if (const auto relatesToJson = contentPart<QJsonObject>("m.relates_to"_ls); + !relatesToJson.isEmpty()) { auto content = eventObject["content"].toObject(); - content["m.relates_to"] = relatesToJson.toObject(); + content["m.relates_to"] = relatesToJson; eventObject["content"] = content; } - if (const auto redactsJson = unsignedPart("redacts"_ls); !redactsJson.isUndefined()) { + if (const auto redactsJson = unsignedPart<QString>("redacts"_ls); + !redactsJson.isEmpty()) { auto unsign = eventObject["unsigned"].toObject(); - unsign["redacts"] = redactsJson.toString(); + unsign["redacts"] = redactsJson; eventObject["unsigned"] = unsign; } return loadEvent<RoomEvent>(eventObject); @@ -64,7 +66,7 @@ RoomEventPtr EncryptedEvent::createDecrypted(const QString &decrypted) const void EncryptedEvent::setRelation(const QJsonObject& relation) { - auto content = editJson()["content"_ls].toObject(); + auto content = contentJson(); content["m.relates_to"] = relation; editJson()["content"] = content; } diff --git a/lib/events/encryptedevent.h b/lib/events/encryptedevent.h index bfacdec9..ddd5e415 100644 --- a/lib/events/encryptedevent.h +++ b/lib/events/encryptedevent.h @@ -58,8 +58,6 @@ public: RoomEventPtr createDecrypted(const QString &decrypted) const; void setRelation(const QJsonObject& relation); - - bool isVerified(); }; REGISTER_EVENT_TYPE(EncryptedEvent) diff --git a/lib/events/encryptedfile.cpp b/lib/events/encryptedfile.cpp deleted file mode 100644 index bb4e26c7..00000000 --- a/lib/events/encryptedfile.cpp +++ /dev/null @@ -1,118 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> -// -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "encryptedfile.h" -#include "logging.h" - -#ifdef Quotient_E2EE_ENABLED -#include <openssl/evp.h> -#include <QtCore/QCryptographicHash> -#include "e2ee/qolmutils.h" -#endif - -using namespace Quotient; - -QByteArray EncryptedFile::decryptFile(const QByteArray& ciphertext) const -{ -#ifdef Quotient_E2EE_ENABLED - auto _key = key.k; - const auto keyBytes = QByteArray::fromBase64( - _key.replace(u'_', u'/').replace(u'-', u'+').toLatin1()); - const auto sha256 = QByteArray::fromBase64(hashes["sha256"].toLatin1()); - if (sha256 - != QCryptographicHash::hash(ciphertext, QCryptographicHash::Sha256)) { - qCWarning(E2EE) << "Hash verification failed for file"; - return {}; - } - { - 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(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<EncryptedFile, QByteArray> EncryptedFile::encryptFile(const QByteArray &plainText) -{ -#ifdef Quotient_E2EE_ENABLED - QByteArray k = getRandom(32); - auto kBase64 = k.toBase64(); - QByteArray iv = getRandom(16); - JWK key = {"oct"_ls, {"encrypt"_ls, "decrypt"_ls}, "A256CTR"_ls, QString(k.toBase64()).replace(u'/', u'_').replace(u'+', u'-').left(kBase64.indexOf('=')), true}; - - int length; - auto* ctx = EVP_CIPHER_CTX_new(); - QByteArray cipherText(plainText.size(), plainText.size() + EVP_MAX_BLOCK_LENGTH - 1); - EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, reinterpret_cast<const unsigned char*>(k.data()),reinterpret_cast<const unsigned char*>(iv.data())); - 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(); - auto ivBase64 = iv.toBase64(); - EncryptedFile file = {{}, key, ivBase64.left(ivBase64.indexOf('=')), {{QStringLiteral("sha256"), hash.left(hash.indexOf('='))}}, "v2"_ls}; - return {file, cipherText}; -#else - return {{}, {}}; -#endif -} - -void JsonObjectConverter<EncryptedFile>::dumpTo(QJsonObject& jo, - const EncryptedFile& pod) -{ - addParam<>(jo, QStringLiteral("url"), pod.url); - addParam<>(jo, QStringLiteral("key"), pod.key); - addParam<>(jo, QStringLiteral("iv"), pod.iv); - addParam<>(jo, QStringLiteral("hashes"), pod.hashes); - addParam<>(jo, QStringLiteral("v"), pod.v); -} - -void JsonObjectConverter<EncryptedFile>::fillFrom(const QJsonObject& jo, - EncryptedFile& pod) -{ - fromJson(jo.value("url"_ls), pod.url); - fromJson(jo.value("key"_ls), pod.key); - fromJson(jo.value("iv"_ls), pod.iv); - fromJson(jo.value("hashes"_ls), pod.hashes); - fromJson(jo.value("v"_ls), pod.v); -} - -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); -} diff --git a/lib/events/encryptedfile.h b/lib/events/encryptedfile.h deleted file mode 100644 index b2808395..00000000 --- a/lib/events/encryptedfile.h +++ /dev/null @@ -1,63 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> -// -// SPDX-License-Identifier: LGPl-2.1-or-later - -#pragma once - -#include "converters.h" - -namespace Quotient { -/** - * JSON Web Key object as specified in - * https://spec.matrix.org/unstable/client-server-api/#extensions-to-mroommessage-msgtypes - * The only currently relevant member is `k`, the rest needs to be set to the defaults specified in the spec. - */ -struct JWK -{ - Q_GADGET - Q_PROPERTY(QString kty MEMBER kty CONSTANT) - Q_PROPERTY(QStringList keyOps MEMBER keyOps CONSTANT) - Q_PROPERTY(QString alg MEMBER alg CONSTANT) - Q_PROPERTY(QString k MEMBER k CONSTANT) - Q_PROPERTY(bool ext MEMBER ext CONSTANT) - -public: - QString kty; - QStringList keyOps; - QString alg; - QString k; - bool ext; -}; - -struct QUOTIENT_API EncryptedFile -{ - Q_GADGET - Q_PROPERTY(QUrl url MEMBER url CONSTANT) - Q_PROPERTY(JWK key MEMBER key CONSTANT) - Q_PROPERTY(QString iv MEMBER iv CONSTANT) - Q_PROPERTY(QHash<QString, QString> hashes MEMBER hashes CONSTANT) - Q_PROPERTY(QString v MEMBER v CONSTANT) - -public: - QUrl url; - JWK key; - QString iv; - QHash<QString, QString> hashes; - QString v; - - QByteArray decryptFile(const QByteArray &ciphertext) const; - static std::pair<EncryptedFile, QByteArray> encryptFile(const QByteArray& plainText); -}; - -template <> -struct QUOTIENT_API JsonObjectConverter<EncryptedFile> { - static void dumpTo(QJsonObject& jo, const EncryptedFile& pod); - static void fillFrom(const QJsonObject& jo, EncryptedFile& pod); -}; - -template <> -struct QUOTIENT_API JsonObjectConverter<JWK> { - static void dumpTo(QJsonObject& jo, const JWK& pod); - static void fillFrom(const QJsonObject& jo, JWK& pod); -}; -} // namespace Quotient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp index 6272c668..8872447b 100644 --- a/lib/events/encryptionevent.cpp +++ b/lib/events/encryptionevent.cpp @@ -6,52 +6,47 @@ #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(EncryptionType et) +EncryptionEventContent::EncryptionEventContent(Quotient::EncryptionType et) : encryption(et) { - if(encryption != Undefined) { - algorithm = encryptionStrings[encryption]; + 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 124ced33..91452c3f 100644 --- a/lib/events/encryptionevent.h +++ b/lib/events/encryptionevent.h @@ -4,57 +4,49 @@ #pragma once -#include "eventcontent.h" -#include "stateevent.h" #include "quotient_common.h" +#include "stateevent.h" namespace Quotient { -class QUOTIENT_API 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; - QUO_IMPLICIT EncryptionEventContent(EncryptionType et); - [[deprecated("This constructor will require explicit EncryptionType soon")]] // - explicit EncryptionEventContent() - : EncryptionEventContent(Undefined) - {} + // 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 QUOTIENT_API EncryptionEvent : public StateEvent<EncryptionEventContent> { - Q_GADGET public: DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) - using EncryptionType = EncryptionEventContent::EncryptionType; - Q_ENUM(EncryptionType) + using EncryptionType + [[deprecated("Use Quotient::EncryptionType instead")]] = + Quotient::EncryptionType; explicit EncryptionEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - [[deprecated("This constructor will require an explicit parameter soon")]] // -// explicit EncryptionEvent() -// : EncryptionEvent(QJsonObject()) -// {} explicit EncryptionEvent(EncryptionEventContent&& content) : StateEvent(typeId(), matrixTypeId(), QString(), std::move(content)) {} - EncryptionType encryption() const { return content().encryption; } - + 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; } + + 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 4c304a3c..1f1eebaa 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -29,7 +29,7 @@ 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(type, basicJson(matrixType, contentJson)) {} Event::~Event() = default; diff --git a/lib/events/event.h b/lib/events/event.h index 113fa3fa..b7454337 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -48,13 +48,6 @@ const QString RoomIdKey { RoomIdKeyL }; const QString UnsignedKey { UnsignedKeyL }; const QString StateKeyKey { StateKeyKeyL }; -/// Make a minimal correct Matrix event JSON -inline QJsonObject basicEventJson(const QString& matrixType, - const QJsonObject& content) -{ - return { { TypeKey, matrixType }, { ContentKey, content } }; -} - // === Event types === using event_type_t = QLatin1String; @@ -193,6 +186,13 @@ public: Event& operator=(Event&&) = delete; virtual ~Event(); + /// Make a minimal correct Matrix event JSON + static QJsonObject basicJson(const QString& matrixType, + const QJsonObject& content) + { + return { { TypeKey, matrixType }, { ContentKey, content } }; + } + Type type() const { return _type; } QString matrixType() const; [[deprecated("Use fullJson() and stringify it with QJsonDocument::toJson() " @@ -212,7 +212,7 @@ public: const QJsonObject contentJson() const; - template <typename T = QJsonValue, typename KeyT> + template <typename T, typename KeyT> const T contentPart(KeyT&& key) const { return fromJson<T>(contentJson()[std::forward<KeyT>(key)]); @@ -227,7 +227,7 @@ public: const QJsonObject unsignedJson() const; - template <typename T = QJsonValue, typename KeyT> + template <typename T, typename KeyT> const T unsignedPart(KeyT&& key) const { return fromJson<T>(unsignedJson()[std::forward<KeyT>(key)]); @@ -258,6 +258,21 @@ template <typename EventT> using EventsArray = std::vector<event_ptr_tt<EventT>>; using Events = EventsArray<Event>; +//! \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_) \ + PartType_ PartName_() const \ + { \ + static const auto JsonKey = toSnakeCase(#PartName_##_ls); \ + return contentPart<PartType_>(JsonKey); \ + } + // === Facilities for event class definitions === // This macro should be used in a public section of an event class to @@ -278,6 +293,32 @@ using Events = EventsArray<Event>; Type_::factory.addMethod<Type_>(); \ // End of macro +/// \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_) \ + class QUOTIENT_API Name_ : public Base_ { \ + public: \ + using content_type = ValueType_; \ + DEFINE_EVENT_TYPEID(TypeId_, Name_) \ + explicit Name_(const QJsonObject& obj) : Base_(TypeId, obj) {} \ + explicit Name_(const content_type& content) \ + : Name_(Base_::basicJson(TypeId, { { JsonKey, toJson(content) } })) \ + {} \ + auto GetterName_() const \ + { \ + return contentPart<content_type>(JsonKey); \ + } \ + static inline const auto JsonKey = toSnakeCase(#GetterName_##_ls); \ + }; \ + REGISTER_EVENT_TYPE(Name_) \ + // End of macro + // === is<>(), eventCast<>() and switchOnType<>() === template <class EventT> @@ -291,65 +332,72 @@ inline bool isUnknown(const Event& e) return e.type() == UnknownEventTypeId; } +//! \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 <class 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 trivial generic catch-all "switch" -template <class BaseEventT, typename FnT> -inline auto switchOnType(const BaseEventT& event, FnT&& fn) - -> decltype(fn(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 <class EventT, typename BaseEventT> +inline auto eventCast(event_ptr_tt<BaseEventT>&& eptr) { - return fn(event); + return eptr && is<std::decay_t<EventT>>(*eptr) + ? event_ptr_tt<EventT>(static_cast<EventT*>(eptr.release())) + : nullptr; } namespace _impl { - // Using bool instead of auto below because auto apparently upsets MSVC - template <class BaseT, typename FnT> - constexpr bool needs_downcast = - std::is_base_of_v<BaseT, std::decay_t<fn_arg_t<FnT>>> - && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>; + template <typename FnT, class BaseT> + concept Invocable_With_Downcast = + std::is_base_of_v<BaseT, std::remove_cvref_t<fn_arg_t<FnT>>>; } -// A trivial type-specific "switch" for a void function -template <class BaseT, typename FnT> -inline auto switchOnType(const BaseT& event, FnT&& fn) - -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT> - && std::is_void_v<fn_return_t<FnT>>> +template <class 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)) - fn(static_cast<event_type>(event)); -} - -// A trivial type-specific "switch" for non-void functions with an optional -// default value; non-voidness is guarded by defaultValue type -template <class BaseT, typename FnT> -inline auto switchOnType(const BaseT& event, FnT&& fn, - fn_return_t<FnT>&& defaultValue = {}) - -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>> -{ - using event_type = fn_arg_t<FnT>; - if (is<std::decay_t<event_type>>(event)) - return fn(static_cast<event_type>(event)); - return std::move(defaultValue); + 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 switch for a chain of 2 or more functions -template <class BaseT, typename FnT1, typename FnT2, typename... FnTs> -inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> -switchOnType(const BaseT& event, FnT1&& fn1, FnT2&& fn2, FnTs&&... fns) +template <class BaseT, typename FnT1, typename... FnTs> +inline auto switchOnType(const BaseT& event, FnT1&& fn1, FnTs&&... fns) { 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<FnT2>(fn2), - std::forward<FnTs>(fns)...); + return fn1(static_cast<event_type1>(event)); + return switchOnType(event, std::forward<FnTs>(fns)...); } template <class BaseT, typename... FnTs> @@ -364,8 +412,8 @@ inline auto visit(const BaseT& event, FnTs&&... fns) // TODO: replace with ranges::for_each once all standard libraries have it template <typename RangeT, typename... FnTs> inline auto visitEach(RangeT&& events, FnTs&&... fns) - -> std::enable_if_t<std::is_void_v< - decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))>> + requires std::is_void_v< + decltype(switchOnType(**begin(events), std::forward<FnTs>(fns)...))> { for (auto&& evtPtr: events) switchOnType(*evtPtr, std::forward<FnTs>(fns)...); diff --git a/lib/events/eventcontent.cpp b/lib/events/eventcontent.cpp index 9d7edf20..8db3b7e3 100644 --- a/lib/events/eventcontent.cpp +++ b/lib/events/eventcontent.cpp @@ -15,26 +15,25 @@ using std::move; QJsonObject Base::toJson() const { QJsonObject o; - fillJson(&o); + fillJson(o); return o; } -FileInfo::FileInfo(const QFileInfo &fi) - : mimeType(QMimeDatabase().mimeTypeForFile(fi)) - , url(QUrl::fromLocalFile(fi.filePath())) - , payloadSize(fi.size()) - , originalName(fi.fileName()) +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(QUrl u, qint64 payloadSize, const QMimeType& mimeType, - Omittable<EncryptedFile> file, QString originalFilename) - : mimeType(mimeType) - , url(move(u)) +FileInfo::FileInfo(FileSourceInfo sourceInfo, qint64 payloadSize, + const QMimeType& mimeType, QString originalFilename) + : source(move(sourceInfo)) + , mimeType(mimeType) , payloadSize(payloadSize) , originalName(move(originalFilename)) - , file(file) { if (!isValid()) qCWarning(MESSAGES) @@ -43,77 +42,81 @@ FileInfo::FileInfo(QUrl u, qint64 payloadSize, const QMimeType& mimeType, "0.7; for local resources, use FileInfo(QFileInfo) instead"; } -FileInfo::FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - const Omittable<EncryptedFile> &file, +FileInfo::FileInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, QString originalFilename) - : originalInfoJson(infoJson) + : source(move(sourceInfo)) + , originalInfoJson(infoJson) , mimeType( QMimeDatabase().mimeTypeForName(infoJson["mimetype"_ls].toString())) - , url(move(mxcUrl)) , payloadSize(fromJson<qint64>(infoJson["size"_ls])) , originalName(move(originalFilename)) - , file(file) { - if(url.isEmpty() && file.has_value()) { - url = file->url; - } if (!mimeType.isValid()) mimeType = QMimeDatabase().mimeTypeForData(QByteArray()); } 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()); - //TODO add encryptedfile + return getUrlFromSourceInfo(source); +} + +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(const QUrl& mxcUrl, qint64 fileSize, const QMimeType& type, - QSize imageSize, const Omittable<EncryptedFile> &file, const QString& originalFilename) - : FileInfo(mxcUrl, fileSize, type, file, originalFilename) +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& mxcUrl, const QJsonObject& infoJson, - const Omittable<EncryptedFile> &file, +ImageInfo::ImageInfo(FileSourceInfo sourceInfo, const QJsonObject& infoJson, const QString& originalFilename) - : FileInfo(mxcUrl, infoJson, file, 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, const Omittable<EncryptedFile> &file) +Thumbnail::Thumbnail(const QJsonObject& infoJson, + const Omittable<EncryptedFileMetadata>& efm) : ImageInfo(QUrl(infoJson["thumbnail_url"_ls].toString()), - infoJson["thumbnail_info"_ls].toObject(), - file) -{} + 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 de9a792b..af26c0a4 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -6,279 +6,249 @@ // This file contains generic event content definitions, applicable to room // message events as well as other events (e.g., avatars). -#include "encryptedfile.h" +#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> class QFileInfo; -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 QUOTIENT_API 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 (the definitions are - // spread across eventcontent.h and roommessageevent.h): - // FileInfo - // FileContent : UrlWithThumbnailContent<FileInfo> - // AudioContent : PlayableContent<UrlBasedContent<FileInfo>> - // ImageInfo : FileInfo + imageSize attribute - // ImageContent : UrlWithThumbnailContent<ImageInfo> - // VideoContent : PlayableContent<UrlWithThumbnailContent<ImageInfo>> - - /** - * 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 QUOTIENT_API FileInfo { - public: - FileInfo() = default; - explicit FileInfo(const QFileInfo& fi); - explicit FileInfo(QUrl mxcUrl, qint64 payloadSize = -1, - const QMimeType& mimeType = {}, - Omittable<EncryptedFile> file = none, - QString originalFilename = {}); - FileInfo(QUrl mxcUrl, const QJsonObject& infoJson, - const Omittable<EncryptedFile> &file, - 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 = 0; - QString originalName; - Omittable<EncryptedFile> file = none; - }; - - template <typename InfoT> - QJsonObject toInfoJson(const InfoT& info) +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 QUOTIENT_API ImageInfo : public FileInfo { - public: - ImageInfo() = default; - explicit ImageInfo(const QFileInfo& fi, QSize imageSize = {}); - explicit ImageInfo(const QUrl& mxcUrl, qint64 fileSize = -1, - const QMimeType& type = {}, QSize imageSize = {}, - const Omittable<EncryptedFile> &file = none, - const QString& originalFilename = {}); - ImageInfo(const QUrl& mxcUrl, const QJsonObject& infoJson, - const Omittable<EncryptedFile> &encryptedFile, - 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 QUOTIENT_API Thumbnail : public ImageInfo { - public: - using ImageInfo::ImageInfo; - Thumbnail(const QJsonObject& infoJson, const Omittable<EncryptedFile> &file = none); - - /** - * Writes thumbnail information to "thumbnail_info" subobject - * and thumbnail URL to "thumbnail_url" node inside "info". - */ - void fillInfoJson(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; - }; - - /** - * 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 QUOTIENT_API UrlBasedContent : public TypedBase, public InfoT { - public: - using InfoT::InfoT; - explicit UrlBasedContent(const QJsonObject& json) - : TypedBase(json) - , InfoT(QUrl(json["url"].toString()), json["info"].toObject(), - fromJson<Omittable<EncryptedFile>>(json["file"]), 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); - if (!InfoT::file.has_value()) { - json->insert("url", InfoT::url.toString()); - } else { - json->insert("file", Quotient::toJson(*InfoT::file)); - } - if (!InfoT::originalName.isEmpty()) - json->insert("filename", InfoT::originalName); - json->insert("info", toInfoJson<InfoT>(*this)); - } - }; - - template <typename InfoT> - class QUOTIENT_API 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 fe624d70..4c639efa 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -19,43 +19,27 @@ inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) return doLoadEvent<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) +//! \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 <typename BaseEventT, typename... BasicJsonParamTs> +inline event_ptr_tt<BaseEventT> loadEvent( + const QString& matrixType, const BasicJsonParamTs&... basicJsonParams) { - return doLoadEvent<BaseEventT>(basicEventJson(matrixType, content), - matrixType); -} - -/*! Create a state event from a type string, content JSON and state key - * - * Use this factory to resolve the C++ type from the Matrix type string - * in \p matrixType and create a state event of that type with content part - * set to \p content and state key set to \p stateKey (empty by default). - */ -inline StateEventPtr loadStateEvent(const QString& matrixType, - const QJsonObject& content, - const QString& stateKey = {}) -{ - return doLoadEvent<StateEventBase>( - basicStateEventJson(matrixType, content, stateKey), matrixType); + return doLoadEvent<BaseEventT>( + BaseEventT::basicJson(matrixType, basicJsonParams...), matrixType); } template <typename EventT> -struct JsonConverter<event_ptr_tt<EventT>> { - static auto load(const QJsonValue& jv) +struct JsonConverter<event_ptr_tt<EventT>> + : JsonObjectUnpacker<event_ptr_tt<EventT>> { + using JsonObjectUnpacker<event_ptr_tt<EventT>>::load; + static auto load(const QJsonObject& jo) { - return loadEvent<EventT>(jv.toObject()); - } - static auto load(const QJsonDocument& jd) - { - return loadEvent<EventT>(jd.object()); + return loadEvent<EventT>(jo); } }; + } // namespace Quotient diff --git a/lib/events/eventrelation.h b/lib/events/eventrelation.h index e445ee42..2a841cf1 100644 --- a/lib/events/eventrelation.h +++ b/lib/events/eventrelation.h @@ -34,11 +34,11 @@ struct QUOTIENT_API EventRelation { return { ReplacementType, std::move(eventId) }; } - [[deprecated("Use ReplyRelation variable instead")]] + [[deprecated("Use ReplyType variable instead")]] static constexpr auto Reply() { return ReplyType; } - [[deprecated("Use AnnotationRelation variable instead")]] // + [[deprecated("Use AnnotationType variable instead")]] // static constexpr auto Annotation() { return AnnotationType; } - [[deprecated("Use ReplacementRelation variable instead")]] // + [[deprecated("Use ReplacementType variable instead")]] // static constexpr auto Replacement() { return ReplacementType; } }; diff --git a/lib/events/filesourceinfo.cpp b/lib/events/filesourceinfo.cpp new file mode 100644 index 00000000..e8b6794b --- /dev/null +++ b/lib/events/filesourceinfo.cpp @@ -0,0 +1,172 @@ +// 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 + QByteArray k = getRandom(32); + auto kBase64 = k.toBase64(); + QByteArray iv = getRandom(16); + JWK key = { "oct"_ls, + { "encrypt"_ls, "decrypt"_ls }, + "A256CTR"_ls, + QString(k.toBase64()) + .replace(u'/', u'_') + .replace(u'+', u'-') + .left(kBase64.indexOf('=')), + true }; + + int length; + auto* ctx = EVP_CIPHER_CTX_new(); + EVP_EncryptInit_ex(ctx, EVP_aes_256_ctr(), nullptr, + reinterpret_cast<const unsigned char*>(k.data()), + reinterpret_cast<const unsigned char*>(iv.data())); + 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(); + auto ivBase64 = iv.toBase64(); + EncryptedFileMetadata efm = { {}, + key, + ivBase64.left(ivBase64.indexOf('=')), + { { QStringLiteral("sha256"), + hash.left(hash.indexOf('=')) } }, + "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.cpp b/lib/events/keyverificationevent.cpp deleted file mode 100644 index e7f5b019..00000000 --- a/lib/events/keyverificationevent.cpp +++ /dev/null @@ -1,194 +0,0 @@ -// SPDX-FileCopyrightText: 2021 Carl Schwan <carlschwan@kde.org> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "keyverificationevent.h" - -using namespace Quotient; - -KeyVerificationRequestEvent::KeyVerificationRequestEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationRequestEvent::fromDevice() const -{ - return contentPart<QString>("from_device"_ls); -} - -QString KeyVerificationRequestEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - -QStringList KeyVerificationRequestEvent::methods() const -{ - return contentPart<QStringList>("methods"_ls); -} - -uint64_t KeyVerificationRequestEvent::timestamp() const -{ - return contentPart<double>("timestamp"_ls); -} - -KeyVerificationStartEvent::KeyVerificationStartEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationStartEvent::fromDevice() const -{ - return contentPart<QString>("from_device"_ls); -} - -QString KeyVerificationStartEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - -QString KeyVerificationStartEvent::method() const -{ - return contentPart<QString>("method"_ls); -} - -Omittable<QString> KeyVerificationStartEvent::nextMethod() const -{ - return contentPart<Omittable<QString>>("method_ls"); -} - -QStringList KeyVerificationStartEvent::keyAgreementProtocols() const -{ - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - return contentPart<QStringList>("key_agreement_protocols"_ls); -} - -QStringList KeyVerificationStartEvent::hashes() const -{ - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - return contentPart<QStringList>("hashes"_ls); - -} - -QStringList KeyVerificationStartEvent::messageAuthenticationCodes() const -{ - Q_ASSERT(method() == QStringLiteral("m.sas.v1")); - return contentPart<QStringList>("message_authentication_codes"_ls); -} - -QString KeyVerificationStartEvent::shortAuthenticationString() const -{ - return contentPart<QString>("short_authentification_string"_ls); -} - -KeyVerificationAcceptEvent::KeyVerificationAcceptEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationAcceptEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - -QString KeyVerificationAcceptEvent::method() const -{ - return contentPart<QString>("method"_ls); -} - -QString KeyVerificationAcceptEvent::keyAgreementProtocol() const -{ - return contentPart<QString>("key_agreement_protocol"_ls); -} - -QString KeyVerificationAcceptEvent::hashData() const -{ - return contentPart<QString>("hash"_ls); -} - -QStringList KeyVerificationAcceptEvent::shortAuthenticationString() const -{ - return contentPart<QStringList>("short_authentification_string"_ls); -} - -QString KeyVerificationAcceptEvent::commitment() const -{ - return contentPart<QString>("commitment"_ls); -} - -KeyVerificationCancelEvent::KeyVerificationCancelEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationCancelEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - -QString KeyVerificationCancelEvent::reason() const -{ - return contentPart<QString>("reason"_ls); -} - -QString KeyVerificationCancelEvent::code() const -{ - return contentPart<QString>("code"_ls); -} - -KeyVerificationKeyEvent::KeyVerificationKeyEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationKeyEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - -QString KeyVerificationKeyEvent::key() const -{ - return contentPart<QString>("key"_ls); -} - -KeyVerificationMacEvent::KeyVerificationMacEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationMacEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - -QString KeyVerificationMacEvent::keys() const -{ - return contentPart<QString>("keys"_ls); -} - -QHash<QString, QString> KeyVerificationMacEvent::mac() const -{ - return contentPart<QHash<QString, QString>>("mac"_ls); -} - -KeyVerificationDoneEvent::KeyVerificationDoneEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{ -} - -QString KeyVerificationDoneEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - - -KeyVerificationReadyEvent::KeyVerificationReadyEvent(const QJsonObject &obj) - : Event(typeId(), obj) -{} - -QString KeyVerificationReadyEvent::fromDevice() const -{ - return contentPart<QString>("from_device"_ls); -} - -QString KeyVerificationReadyEvent::transactionId() const -{ - return contentPart<QString>("transaction_id"_ls); -} - -QStringList KeyVerificationReadyEvent::methods() const -{ - return contentPart<QStringList>("methods"_ls); -} diff --git a/lib/events/keyverificationevent.h b/lib/events/keyverificationevent.h index a9f63968..cdbd5d74 100644 --- a/lib/events/keyverificationevent.h +++ b/lib/events/keyverificationevent.h @@ -7,29 +7,33 @@ namespace Quotient { +static constexpr auto SasV1Method = "m.sas.v1"_ls; + /// Requests a key verification with another user's devices. /// Typically sent as a to-device event. class QUOTIENT_API KeyVerificationRequestEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.request", KeyVerificationRequestEvent) - explicit KeyVerificationRequestEvent(const QJsonObject& obj); + explicit KeyVerificationRequestEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} /// The device ID which is initiating the request. - QString fromDevice() const; + QUO_CONTENT_GETTER(QString, fromDevice) /// An opaque identifier for the verification request. Must /// be unique with respect to the devices involved. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// The verification methods supported by the sender. - QStringList methods() const; + 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. - uint64_t timestamp() const; + QUO_CONTENT_GETTER(QDateTime, timestamp) }; REGISTER_EVENT_TYPE(KeyVerificationRequestEvent) @@ -37,16 +41,18 @@ class QUOTIENT_API KeyVerificationReadyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.ready", KeyVerificationReadyEvent) - explicit KeyVerificationReadyEvent(const QJsonObject& obj); + explicit KeyVerificationReadyEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} /// The device ID which is accepting the request. - QString fromDevice() const; + QUO_CONTENT_GETTER(QString, fromDevice) /// The transaction id of the verification request - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// The verification methods supported by the sender. - QStringList methods() const; + QUO_CONTENT_GETTER(QStringList, methods) }; REGISTER_EVENT_TYPE(KeyVerificationReadyEvent) @@ -56,39 +62,57 @@ class QUOTIENT_API KeyVerificationStartEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.start", KeyVerificationStartEvent) - explicit KeyVerificationStartEvent(const QJsonObject &obj); + explicit KeyVerificationStartEvent(const QJsonObject &obj) + : Event(TypeId, obj) + {} /// The device ID which is initiating the process. - QString fromDevice() const; + QUO_CONTENT_GETTER(QString, fromDevice) /// An opaque identifier for the verification request. Must /// be unique with respect to the devices involved. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// The verification method to use. - QString method() const; + QUO_CONTENT_GETTER(QString, method) /// Optional method to use to verify the other user's key with. - Omittable<QString> nextMethod() const; + 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; + 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; + 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; + 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; + QString shortAuthenticationString() const + { + Q_ASSERT(method() == SasV1Method); + return contentPart<QString>("short_authentification_string"_ls); + } }; REGISTER_EVENT_TYPE(KeyVerificationStartEvent) @@ -98,33 +122,38 @@ class QUOTIENT_API KeyVerificationAcceptEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.accept", KeyVerificationAcceptEvent) - explicit KeyVerificationAcceptEvent(const QJsonObject& obj); + explicit KeyVerificationAcceptEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} /// An opaque identifier for the verification process. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// The verification method to use. Must be 'm.sas.v1'. - QString method() const; + 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. - QString keyAgreementProtocol() const; + 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. - QString hashData() const; + QString hashData() const + { + return contentPart<QString>("hash"_ls); + } /// The message authentication code the device is choosing to use, out /// of the options in the m.key.verification.start message. - QString messageAuthenticationCode() const; + QUO_CONTENT_GETTER(QString, messageAuthenticationCode) /// The SAS methods both devices involved in the verification process understand. - QStringList shortAuthenticationString() const; + 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. - QString commitment() const; + QUO_CONTENT_GETTER(QString, commitment) }; REGISTER_EVENT_TYPE(KeyVerificationAcceptEvent) @@ -132,17 +161,19 @@ class QUOTIENT_API KeyVerificationCancelEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.cancel", KeyVerificationCancelEvent) - explicit KeyVerificationCancelEvent(const QJsonObject &obj); + explicit KeyVerificationCancelEvent(const QJsonObject &obj) + : Event(TypeId, obj) + {} /// An opaque identifier for the verification process. - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) /// A human readable description of the code. The client should only /// rely on this string if it does not understand the code. - QString reason() const; + QUO_CONTENT_GETTER(QString, reason) /// The error code for why the process/request was cancelled by the user. - QString code() const; + QUO_CONTENT_GETTER(QString, code) }; REGISTER_EVENT_TYPE(KeyVerificationCancelEvent) @@ -152,13 +183,15 @@ class QUOTIENT_API KeyVerificationKeyEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.key", KeyVerificationKeyEvent) - explicit KeyVerificationKeyEvent(const QJsonObject &obj); + explicit KeyVerificationKeyEvent(const QJsonObject &obj) + : Event(TypeId, obj) + {} - /// An opaque identifier for the verification process. - QString transactionId() const; + /// An opaque identifier for the verification process. + QUO_CONTENT_GETTER(QString, transactionId) /// The device's ephemeral public key, encoded as unpadded base64. - QString key() const; + QUO_CONTENT_GETTER(QString, key) }; REGISTER_EVENT_TYPE(KeyVerificationKeyEvent) @@ -167,15 +200,20 @@ class QUOTIENT_API KeyVerificationMacEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.mac", KeyVerificationMacEvent) - explicit KeyVerificationMacEvent(const QJsonObject &obj); + explicit KeyVerificationMacEvent(const QJsonObject &obj) + : Event(TypeId, obj) + {} - /// An opaque identifier for the verification process. - QString transactionId() const; + /// An opaque identifier for the verification process. + QUO_CONTENT_GETTER(QString, transactionId) /// The device's ephemeral public key, encoded as unpadded base64. - QString keys() const; + QUO_CONTENT_GETTER(QString, keys) - QHash<QString, QString> mac() const; + QHash<QString, QString> mac() const + { + return contentPart<QHash<QString, QString>>("mac"_ls); + } }; REGISTER_EVENT_TYPE(KeyVerificationMacEvent) @@ -183,10 +221,12 @@ class QUOTIENT_API KeyVerificationDoneEvent : public Event { public: DEFINE_EVENT_TYPEID("m.key.verification.done", KeyVerificationRequestEvent) - explicit KeyVerificationDoneEvent(const QJsonObject& obj); + explicit KeyVerificationDoneEvent(const QJsonObject& obj) + : Event(TypeId, obj) + {} /// The same transactionId as before - QString transactionId() const; + QUO_CONTENT_GETTER(QString, transactionId) }; REGISTER_EVENT_TYPE(KeyVerificationDoneEvent) diff --git a/lib/events/redactionevent.h b/lib/events/redactionevent.h index be20bf52..63617e54 100644 --- a/lib/events/redactionevent.h +++ b/lib/events/redactionevent.h @@ -6,7 +6,7 @@ #include "roomevent.h" namespace Quotient { -class RedactionEvent : public RoomEvent { +class QUOTIENT_API RedactionEvent : public RoomEvent { public: DEFINE_EVENT_TYPEID("m.room.redaction", RedactionEvent) @@ -17,7 +17,7 @@ public: { return fullJson()["redacts"_ls].toString(); } - QString reason() const { return contentPart<QString>("reason"_ls); } + QUO_CONTENT_GETTER(QString, reason) }; REGISTER_EVENT_TYPE(RedactionEvent) } // namespace Quotient diff --git a/lib/events/roomavatarevent.h b/lib/events/roomavatarevent.h index c54b5801..af291696 100644 --- a/lib/events/roomavatarevent.h +++ b/lib/events/roomavatarevent.h @@ -26,10 +26,10 @@ public: const QSize& imageSize = {}, const QString& originalFilename = {}) : RoomAvatarEvent(EventContent::ImageContent { - mxcUrl, fileSize, mimeType, imageSize, none, originalFilename }) + mxcUrl, fileSize, mimeType, imageSize, originalFilename }) {} - 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 bb8654e5..60ca68ac 100644 --- a/lib/events/roomcanonicalaliasevent.h +++ b/lib/events/roomcanonicalaliasevent.h @@ -7,36 +7,31 @@ #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 +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 StateEvent<EventContent::AliasesEventContent> { public: DEFINE_EVENT_TYPEID("m.room.canonical_alias", RoomCanonicalAliasEvent) diff --git a/lib/events/roomcreateevent.cpp b/lib/events/roomcreateevent.cpp index bb6de648..3b5024d5 100644 --- a/lib/events/roomcreateevent.cpp +++ b/lib/events/roomcreateevent.cpp @@ -6,20 +6,11 @@ using namespace Quotient; template <> -struct Quotient::JsonConverter<RoomType> { - static RoomType load(const QJsonValue& jv) - { - const auto& roomTypeString = jv.toString(); - for (auto it = RoomTypeStrings.begin(); it != RoomTypeStrings.end(); - ++it) - if (roomTypeString == *it) - return RoomType(it - RoomTypeStrings.begin()); - - if (!roomTypeString.isEmpty()) - qCWarning(EVENTS) << "Unknown Room Type: " << roomTypeString; - return RoomType::Undefined; - } -}; +RoomType Quotient::fromJson(const QJsonValue& jv) +{ + return enumFromJsonString(jv.toString(), RoomTypeStrings, + RoomType::Undefined); +} bool RoomCreateEvent::isFederated() const { diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index 2f482871..e695e0ec 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -15,9 +15,9 @@ RoomEvent::RoomEvent(Type type, event_mtype_t matrixType, RoomEvent::RoomEvent(Type type, const QJsonObject& json) : Event(type, json) { - if (const auto redaction = unsignedPart(RedactedCauseKeyL); - redaction.isObject()) - _redactedBecause = makeEvent<RedactionEvent>(redaction.toObject()); + if (const auto redaction = unsignedPart<QJsonObject>(RedactedCauseKeyL); + !redaction.isEmpty()) + _redactedBecause = makeEvent<RedactionEvent>(redaction); } RoomEvent::~RoomEvent() = default; // Let the smart pointer do its job @@ -101,22 +101,22 @@ void RoomEvent::dumpTo(QDebug dbg) const dbg << " (made at " << originTimestamp().toString(Qt::ISODate) << ')'; } -QJsonObject makeCallContentJson(const QString& callId, int version, - QJsonObject content) +QJsonObject CallEventBase::basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson) { - content.insert(QStringLiteral("call_id"), callId); - content.insert(QStringLiteral("version"), version); - return content; + contentJson.insert(QStringLiteral("call_id"), callId); + contentJson.insert(QStringLiteral("version"), version); + return RoomEvent::basicJson(matrixType, contentJson); } CallEventBase::CallEventBase(Type type, event_mtype_t matrixType, const QString& callId, int version, const QJsonObject& contentJson) - : RoomEvent(type, matrixType, - makeCallContentJson(callId, version, contentJson)) + : RoomEvent(type, basicJson(matrixType, callId, version, contentJson)) {} -CallEventBase::CallEventBase(Event::Type type, const QJsonObject& json) +CallEventBase::CallEventBase(Type type, const QJsonObject& json) : RoomEvent(type, json) { if (callId().isEmpty()) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 5670f55f..9461340b 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -96,8 +96,13 @@ public: ~CallEventBase() override = default; bool isCallEvent() const override { return true; } - QString callId() const { return contentPart<QString>("call_id"_ls); } - int version() const { return contentPart<int>("version"_ls); } + QUO_CONTENT_GETTER(QString, callId) + QUO_CONTENT_GETTER(int, version) + +protected: + static QJsonObject basicJson(const QString& matrixType, + const QString& callId, int version, + QJsonObject contentJson = {}); }; } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) diff --git a/lib/events/roomkeyevent.h b/lib/events/roomkeyevent.h index cb3fe7e7..9eb2854b 100644 --- a/lib/events/roomkeyevent.h +++ b/lib/events/roomkeyevent.h @@ -12,12 +12,17 @@ public: DEFINE_EVENT_TYPEID("m.room_key", RoomKeyEvent) explicit RoomKeyEvent(const QJsonObject& obj); - explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, const QString &sessionId, const QString& sessionKey, const QString& senderId); + explicit RoomKeyEvent(const QString& algorithm, const QString& roomId, + const QString& sessionId, const QString& sessionKey, + const QString& senderId); - QString algorithm() const { return contentPart<QString>("algorithm"_ls); } - QString roomId() const { return contentPart<QString>(RoomIdKeyL); } - QString sessionId() const { return contentPart<QString>("session_id"_ls); } - QString sessionKey() const { return contentPart<QString>("session_key"_ls); } + 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 b4770224..953ff8ae 100644 --- a/lib/events/roommemberevent.cpp +++ b/lib/events/roommemberevent.cpp @@ -4,8 +4,6 @@ #include "roommemberevent.h" -#include "logging.h" - #include <QtCore/QtAlgorithms> namespace Quotient { @@ -13,18 +11,10 @@ template <> struct JsonConverter<Membership> { static Membership load(const QJsonValue& jv) { - const auto& ms = jv.toString(); - if (ms.isEmpty()) - { - qCWarning(EVENTS) << "Empty membership state"; - return Membership::Invalid; - } - const auto it = - std::find(MembershipStrings.begin(), MembershipStrings.end(), ms); - if (it != MembershipStrings.end()) - return Membership(1U << (it - MembershipStrings.begin())); - - qCWarning(EVENTS) << "Unknown Membership value: " << ms; + if (const auto& ms = jv.toString(); !ms.isEmpty()) + return flagFromJsonString<Membership>(ms, MembershipStrings); + + qCWarning(EVENTS) << "Empty membership state"; return Membership::Invalid; } }; @@ -43,19 +33,19 @@ MemberEventContent::MemberEventContent(const QJsonObject& json) displayName = sanitized(*displayName); } -void MemberEventContent::fillJson(QJsonObject* o) const +QJsonObject MemberEventContent::toJson() const { - Q_ASSERT(o); + QJsonObject o; if (membership != Membership::Invalid) - o->insert(QStringLiteral("membership"), - MembershipStrings[qCountTrailingZeroBits( - std::underlying_type_t<Membership>(membership))]); + o.insert(QStringLiteral("membership"), + flagToJsonString(membership, MembershipStrings)); if (displayName) - o->insert(QStringLiteral("displayname"), *displayName); + o.insert(QStringLiteral("displayname"), *displayName); if (avatarUrl && avatarUrl->isValid()) - o->insert(QStringLiteral("avatar_url"), avatarUrl->toString()); + 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 diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index ceb7826b..dd33ea6b 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -5,18 +5,18 @@ #pragma once -#include "eventcontent.h" #include "stateevent.h" #include "quotient_common.h" namespace Quotient { -class QUOTIENT_API MemberEventContent : public EventContent::Base { +class QUOTIENT_API MemberEventContent { public: using MembershipType [[deprecated("Use Quotient::Membership instead")]] = Membership; QUO_IMPLICIT MemberEventContent(Membership ms) : membership(ms) {} explicit MemberEventContent(const QJsonObject& json); + QJsonObject toJson() const; Membership membership; /// (Only for invites) Whether the invite is to a direct chat @@ -24,9 +24,6 @@ public: Omittable<QString> displayName; Omittable<QUrl> avatarUrl; QString reason; - -protected: - void fillJson(QJsonObject* o) const override; }; using MembershipType [[deprecated("Use Membership instead")]] = Membership; diff --git a/lib/events/roommessageevent.cpp b/lib/events/roommessageevent.cpp index d63352cb..2a6ae93c 100644 --- a/lib/events/roommessageevent.cpp +++ b/lib/events/roommessageevent.cpp @@ -148,21 +148,21 @@ TypedBase* contentFromFile(const QFileInfo& file, bool asGenericFile) auto mimeTypeName = mimeType.name(); if (mimeTypeName.startsWith("image/")) return new ImageContent(localUrl, file.size(), mimeType, - QImageReader(filePath).size(), none, + QImageReader(filePath).size(), file.fileName()); // duration can only be obtained asynchronously and can only be reliably // done by starting to play the file. Left for a future implementation. if (mimeTypeName.startsWith("video/")) return new VideoContent(localUrl, file.size(), mimeType, - QMediaResource(localUrl).resolution(), none, + QMediaResource(localUrl).resolution(), file.fileName()); if (mimeTypeName.startsWith("audio/")) - return new AudioContent(localUrl, file.size(), mimeType, none, + return new AudioContent(localUrl, file.size(), mimeType, file.fileName()); } - return new FileContent(localUrl, file.size(), mimeType, none, file.fileName()); + return new FileContent(localUrl, file.size(), mimeType, file.fileName()); } RoomMessageEvent::RoomMessageEvent(const QString& plainBody, @@ -302,17 +302,16 @@ TextContent::TextContent(const QJsonObject& json) } } -void TextContent::fillJson(QJsonObject* json) const +void TextContent::fillJson(QJsonObject &json) const { static const auto FormatKey = QStringLiteral("format"); - 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( + json.insert( QStringLiteral("m.relates_to"), relatesTo->type == EventRelation::ReplyType ? QJsonObject { { relatesTo->type, @@ -326,7 +325,7 @@ void TextContent::fillJson(QJsonObject* json) const newContentJson.insert(FormatKey, HtmlContentTypeId); newContentJson.insert(FormattedBodyKey, body); } - json->insert(QStringLiteral("m.new_content"), newContentJson); + json.insert(QStringLiteral("m.new_content"), newContentJson); } } } @@ -347,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 03a51328..6968ad70 100644 --- a/lib/events/roommessageevent.h +++ b/lib/events/roommessageevent.h @@ -136,7 +136,7 @@ namespace EventContent { Omittable<EventRelation> relatesTo; protected: - void fillJson(QJsonObject* json) const override; + void fillJson(QJsonObject& json) const override; }; /** @@ -164,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 QUOTIENT_API 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: @@ -211,7 +208,7 @@ namespace EventContent { * - mimeType * - imageSize */ - using VideoContent = PlayableContent<UrlWithThumbnailContent<ImageInfo>>; + using VideoContent = PlayableContent<ImageInfo>; /** * Content class for m.audio @@ -224,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 8d262ddf..d9bd010b 100644 --- a/lib/events/roompowerlevelsevent.cpp +++ b/lib/events/roompowerlevelsevent.cpp @@ -3,10 +3,10 @@ #include "roompowerlevelsevent.h" -#include <QJsonDocument> - 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)), @@ -18,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 415cc814..a1638a27 100644 --- a/lib/events/roompowerlevelsevent.h +++ b/lib/events/roompowerlevelsevent.h @@ -3,17 +3,16 @@ #pragma once -#include "eventcontent.h" #include "stateevent.h" namespace Quotient { -class QUOTIENT_API 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; @@ -29,9 +28,6 @@ public: int usersDefault; Notifications notifications; - -protected: - void fillJson(QJsonObject* o) const override; }; class QUOTIENT_API RoomPowerLevelsEvent diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 9610574b..a8eaab56 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -4,65 +4,51 @@ #pragma once #include "stateevent.h" +#include "single_key_value.h" namespace Quotient { -namespace EventContent { - template <typename T> - struct SimpleContent { - 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) } }; - } - - T value; - const QString key; - }; -} // namespace EventContent - -#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ - class QUOTIENT_API _Name \ - : public StateEvent<EventContent::SimpleContent<_ValueType>> { \ - public: \ - using value_type = content_type::value_type; \ - DEFINE_EVENT_TYPEID(_TypeId, _Name) \ - 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) \ +#define DEFINE_SIMPLE_STATE_EVENT(_Name, _TypeId, _ValueType, _ContentKey) \ + constexpr auto _Name##Key = #_ContentKey##_ls; \ + class QUOTIENT_API _Name \ + : public StateEvent< \ + EventContent::SingleKeyValue<_ValueType, &_Name##Key>> { \ + public: \ + using value_type = _ValueType; \ + DEFINE_EVENT_TYPEID(_TypeId, _Name) \ + template <typename T> \ + explicit _Name(T&& value) \ + : StateEvent(TypeId, matrixTypeId(), QString(), \ + std::forward<T>(value)) \ + {} \ + explicit _Name(QJsonObject obj) \ + : StateEvent(TypeId, std::move(obj)) \ + {} \ + auto _ContentKey() const { return content().value; } \ + }; \ + REGISTER_EVENT_TYPE(_Name) \ // End of macro DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) -DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", QStringList, pinnedEvents) +DEFINE_SIMPLE_STATE_EVENT(RoomPinnedEvent, "m.room.pinned_messages", + QStringList, pinnedEvents) -class [[deprecated( - "m.room.aliases events are deprecated by the Matrix spec; use" - " RoomCanonicalAliasEvent::altAliases() to get non-authoritative aliases")]] // -RoomAliasesEvent : public StateEvent<EventContent::SimpleContent<QStringList>> { +constexpr auto RoomAliasesEventKey = "aliases"_ls; +class QUOTIENT_API RoomAliasesEvent + : public StateEvent< + EventContent::SingleKeyValue<QStringList, &RoomAliasesEventKey>> { public: DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) explicit RoomAliasesEvent(const QJsonObject& obj) - : StateEvent(typeId(), obj, QStringLiteral("aliases")) + : StateEvent(typeId(), obj) {} + 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; } }; } // namespace Quotient diff --git a/lib/events/single_key_value.h b/lib/events/single_key_value.h new file mode 100644 index 00000000..75ca8cd2 --- /dev/null +++ b/lib/events/single_key_value.h @@ -0,0 +1,27 @@ +#pragma once + +#include "converters.h" + +namespace Quotient { + +namespace EventContent { + template <typename T, const QLatin1String* KeyStr> + struct SingleKeyValue { + 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 e53d47d4..1df24df0 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -6,9 +6,9 @@ using namespace Quotient; StateEventBase::StateEventBase(Type type, const QJsonObject& json) - : RoomEvent(json.contains(StateKeyKeyL) ? type : unknownEventTypeId(), json) + : RoomEvent(json.contains(StateKeyKeyL) ? type : UnknownEventTypeId, json) { - if (Event::type() == unknownEventTypeId() && !json.contains(StateKeyKeyL)) + if (Event::type() == UnknownEventTypeId && !json.contains(StateKeyKeyL)) qWarning(EVENTS) << "Attempt to create a state event with no stateKey -" "forcing the event type to unknown to avoid damage"; } @@ -16,12 +16,12 @@ StateEventBase::StateEventBase(Type type, const QJsonObject& json) StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, const QString& stateKey, const QJsonObject& contentJson) - : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey)) + : RoomEvent(type, basicJson(type, stateKey, contentJson)) {} bool StateEventBase::repeatsState() const { - const auto prevContentJson = unsignedPart(PrevContentKeyL); + const auto prevContentJson = unsignedPart<QJsonObject>(PrevContentKeyL); return fullJson().value(ContentKeyL) == prevContentJson; } diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 88da68f8..9f1d7118 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -7,16 +7,6 @@ namespace Quotient { -/// Make a minimal correct Matrix state event JSON -inline QJsonObject basicStateEventJson(const QString& matrixTypeId, - const QJsonObject& content, - const QString& stateKey = {}) -{ - return { { TypeKey, matrixTypeId }, - { StateKeyKey, stateKey }, - { ContentKey, content } }; -} - class QUOTIENT_API StateEventBase : public RoomEvent { public: static inline EventFactory<StateEventBase> factory { "StateEvent" }; @@ -27,6 +17,16 @@ public: const QJsonObject& contentJson = {}); ~StateEventBase() override = default; + //! 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; @@ -36,6 +36,14 @@ public: using StateEventPtr = event_ptr_tt<StateEventBase>; using StateEvents = EventsArray<StateEventBase>; +[[deprecated("Use StateEventBase::basicJson() instead")]] +inline QJsonObject basicStateEventJson(const QString& matrixTypeId, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return StateEventBase::basicJson(matrixTypeId, stateKey, content); +} + //! \brief Override RoomEvent factory with that from StateEventBase if JSON has //! stateKey //! @@ -64,7 +72,7 @@ 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>; +using StateEventKey = std::pair<QString, QString>; template <typename ContentT> struct Prev { @@ -72,7 +80,7 @@ struct Prev { explicit Prev(const QJsonObject& unsignedJson, ContentParamTs&&... contentParams) : senderId(unsignedJson.value("prev_sender"_ls).toString()) - , content(unsignedJson.value(PrevContentKeyL).toObject(), + , content(fromJson<ContentT>(unsignedJson.value(PrevContentKeyL)), std::forward<ContentParamTs>(contentParams)...) {} @@ -89,7 +97,8 @@ public: explicit StateEvent(Type type, const QJsonObject& fullJson, ContentParamTs&&... contentParams) : StateEventBase(type, fullJson) - , _content(contentJson(), std::forward<ContentParamTs>(contentParams)...) + , _content(fromJson<ContentT>(contentJson()), + std::forward<ContentParamTs>(contentParams)...) { const auto& unsignedData = unsignedJson(); if (unsignedData.contains(PrevContentKeyL)) @@ -101,9 +110,9 @@ public: const QString& stateKey, ContentParamTs&&... contentParams) : StateEventBase(type, matrixType, stateKey) - , _content(std::forward<ContentParamTs>(contentParams)...) + , _content{std::forward<ContentParamTs>(contentParams)...} { - editJson().insert(ContentKey, _content.toJson()); + editJson().insert(ContentKey, toJson(_content)); } const ContentT& content() const { return _content; } @@ -111,7 +120,7 @@ public: void editContent(VisitorT&& visitor) { visitor(_content); - editJson()[ContentKeyL] = _content.toJson(); + editJson()[ContentKeyL] = toJson(_content); } const ContentT* prevContent() const { diff --git a/lib/events/stickerevent.cpp b/lib/events/stickerevent.cpp deleted file mode 100644 index 628fd154..00000000 --- a/lib/events/stickerevent.cpp +++ /dev/null @@ -1,26 +0,0 @@ -// SDPX-FileCopyrightText: 2020 Carl Schwan <carlschwan@kde.org> -// SPDX-License-Identifier: LGPL-2.1-or-later - -#include "stickerevent.h" - -using namespace Quotient; - -StickerEvent::StickerEvent(const QJsonObject &obj) - : RoomEvent(typeId(), obj) - , m_imageContent(EventContent::ImageContent(obj["content"_ls].toObject())) -{} - -QString StickerEvent::body() const -{ - return contentPart<QString>("body"_ls); -} - -const EventContent::ImageContent &StickerEvent::image() const -{ - return m_imageContent; -} - -QUrl StickerEvent::url() const -{ - return m_imageContent.url; -} diff --git a/lib/events/stickerevent.h b/lib/events/stickerevent.h index 0957dca3..e378422d 100644 --- a/lib/events/stickerevent.h +++ b/lib/events/stickerevent.h @@ -16,21 +16,32 @@ class QUOTIENT_API StickerEvent : public RoomEvent public: DEFINE_EVENT_TYPEID("m.sticker", StickerEvent) - explicit StickerEvent(const QJsonObject &obj); + 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. - QString body() const; + QUO_CONTENT_GETTER(QString, body) /// \brief Metadata about the image referred to in url including a /// thumbnail representation. - const EventContent::ImageContent &image() const; + 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; + QUrl url() const + { + return m_imageContent.url(); + } + private: EventContent::ImageContent m_imageContent; }; diff --git a/lib/expected.h b/lib/expected.h new file mode 100644 index 00000000..7b9e7f1d --- /dev/null +++ b/lib/expected.h @@ -0,0 +1,77 @@ +// 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; + explicit Expected(const Expected&) = default; + explicit Expected(Expected&&) noexcept = default; + + template <typename X, typename = enable_if_constructible_t<X>> + Expected(X&& x) + : 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 index 6542101a..e3d27122 100644 --- a/lib/function_traits.cpp +++ b/lib/function_traits.cpp @@ -7,6 +7,9 @@ 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<>"); diff --git a/lib/function_traits.h b/lib/function_traits.h index 83b8e425..143ed162 100644 --- a/lib/function_traits.h +++ b/lib/function_traits.h @@ -8,7 +8,7 @@ namespace Quotient { namespace _impl { - template <typename AlwaysVoid, typename> + template <typename> struct fn_traits {}; } @@ -21,73 +21,73 @@ namespace _impl { */ template <typename T> struct function_traits - : public _impl::fn_traits<void, std::remove_reference_t<T>> {}; + : 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...>; - // See also the comment for wrap_in_function() in qt_connection_util.h - using function_type = std::function<ReturnT(ArgTs...)>; }; namespace _impl { - template <typename AlwaysVoid, typename> + template <typename> struct fn_object_traits; // Specialisation for a lambda function template <typename ReturnT, typename ClassT, typename... ArgTs> - struct fn_object_traits<void, ReturnT (ClassT::*)(ArgTs...)> + 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<void, ReturnT (ClassT::*)(ArgTs...) const> + 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> - struct fn_traits<decltype(void(&T::operator())), T> - : public fn_object_traits<void, decltype(&T::operator())> {}; + 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<void, ReturnT (ClassT::*)(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<void, ReturnT (ClassT::*)(ArgTs...) const> + 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<void, ReturnT (ClassT::*)(ArgTs...) const&> + 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<void, ReturnT (ClassT::*)(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<void, ReturnT 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<void, const ReturnT ClassT::*> + struct fn_traits<const ReturnT ClassT::*> : function_traits<const ReturnT&(ClassT)> {}; } // namespace _impl -template <typename FnT> -using fn_return_t = typename function_traits<FnT>::return_type; - template <typename FnT, int ArgN = 0> using fn_arg_t = std::tuple_element_t<ArgN, typename function_traits<FnT>::arg_types>; +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 b6858b5a..da645a2d 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -138,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; @@ -152,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) @@ -301,16 +298,10 @@ void BaseJob::Private::sendRequest() 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()); @@ -754,11 +745,14 @@ QString BaseJob::statusCaption() const } } -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) { diff --git a/lib/jobs/downloadfilejob.cpp b/lib/jobs/downloadfilejob.cpp index d00fc5f4..759d52c9 100644 --- a/lib/jobs/downloadfilejob.cpp +++ b/lib/jobs/downloadfilejob.cpp @@ -8,8 +8,9 @@ #include <QtNetwork/QNetworkReply> #ifdef Quotient_E2EE_ENABLED -# include <QtCore/QCryptographicHash> -# include "events/encryptedfile.h" +# include "events/filesourceinfo.h" + +# include <QtCore/QCryptographicHash> #endif using namespace Quotient; @@ -26,7 +27,7 @@ public: QScopedPointer<QFile> tempFile; #ifdef Quotient_E2EE_ENABLED - Omittable<EncryptedFile> encryptedFile; + Omittable<EncryptedFileMetadata> encryptedFileMetadata; #endif }; @@ -49,14 +50,14 @@ DownloadFileJob::DownloadFileJob(const QString& serverName, #ifdef Quotient_E2EE_ENABLED DownloadFileJob::DownloadFileJob(const QString& serverName, const QString& mediaId, - const EncryptedFile& file, + const EncryptedFileMetadata& file, const QString& localFilename) : GetContentJob(serverName, mediaId) , d(localFilename.isEmpty() ? makeImpl<Private>() : makeImpl<Private>(localFilename)) { setObjectName(QStringLiteral("DownloadFileJob")); - d->encryptedFile = file; + d->encryptedFileMetadata = file; } #endif QString DownloadFileJob::targetFileName() const @@ -118,27 +119,31 @@ 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) { #ifdef Quotient_E2EE_ENABLED - if (d->encryptedFile.has_value()) { - d->tempFile->seek(0); - QByteArray encrypted = d->tempFile->readAll(); - - EncryptedFile file = *d->encryptedFile; - const auto decrypted = file.decryptFile(encrypted); - d->targetFile->write(decrypted); + if (d->encryptedFileMetadata.has_value()) { + decryptFile(*d->tempFile, *d->encryptedFileMetadata, *d->targetFile); d->tempFile->remove(); } else { #endif d->targetFile->close(); if (!d->targetFile->remove()) { - qCWarning(JOBS) << "Failed to remove the target file placeholder"; + qWarning(JOBS) << "Failed to remove the target file placeholder"; return { FileError, "Couldn't finalise the download" }; } if (!d->tempFile->rename(d->targetFile->fileName())) { - qCWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() + qWarning(JOBS) << "Failed to rename" << d->tempFile->fileName() << "to" << d->targetFile->fileName(); return { FileError, "Couldn't finalise the download" }; } @@ -147,13 +152,20 @@ BaseJob::Status DownloadFileJob::prepareResult() #endif } else { #ifdef Quotient_E2EE_ENABLED - if (d->encryptedFile.has_value()) { - d->tempFile->seek(0); - const auto encrypted = d->tempFile->readAll(); - - EncryptedFile file = *d->encryptedFile; - const auto decrypted = file.decryptFile(encrypted); - d->tempFile->write(decrypted); + 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(); @@ -161,6 +173,6 @@ BaseJob::Status DownloadFileJob::prepareResult() } #endif } - qCDebug(JOBS) << "Saved a file as" << targetFileName(); + qDebug(JOBS) << "Saved a file as" << targetFileName(); return Success; } diff --git a/lib/jobs/downloadfilejob.h b/lib/jobs/downloadfilejob.h index ffa3d055..cbbfd244 100644 --- a/lib/jobs/downloadfilejob.h +++ b/lib/jobs/downloadfilejob.h @@ -4,7 +4,8 @@ #pragma once #include "csapi/content-repo.h" -#include "events/encryptedfile.h" + +#include "events/filesourceinfo.h" namespace Quotient { class QUOTIENT_API DownloadFileJob : public GetContentJob { @@ -16,7 +17,7 @@ public: const QString& localFilename = {}); #ifdef Quotient_E2EE_ENABLED - DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFile& file, const QString& localFilename = {}); + DownloadFileJob(const QString& serverName, const QString& mediaId, const EncryptedFileMetadata& file, const QString& localFilename = {}); #endif QString targetFileName() const; diff --git a/lib/jobs/requestdata.cpp b/lib/jobs/requestdata.cpp index 2c001ccc..ab249f6d 100644 --- a/lib/jobs/requestdata.cpp +++ b/lib/jobs/requestdata.cpp @@ -14,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; @@ -33,7 +33,5 @@ RequestData::RequestData(const QJsonObject& jo) : _source(fromJson(jo)) {} RequestData::RequestData(const QJsonArray& ja) : _source(fromJson(ja)) {} RequestData::RequestData(QIODevice* source) - : _source(std::unique_ptr<QIODevice>(source)) + : _source(acquireImpl(source)) {} - -RequestData::~RequestData() = default; diff --git a/lib/jobs/requestdata.h b/lib/jobs/requestdata.h index 41ad833a..accc8f71 100644 --- a/lib/jobs/requestdata.h +++ b/lib/jobs/requestdata.h @@ -3,11 +3,7 @@ #pragma once -#include "quotient_export.h" - -#include <QtCore/QByteArray> - -#include <memory> +#include "util.h" class QJsonObject; class QJsonArray; @@ -23,17 +19,17 @@ namespace Quotient { */ class QUOTIENT_API RequestData { public: - RequestData(const QByteArray& a = {}); - RequestData(const QJsonObject& jo); - RequestData(const QJsonArray& ja); - RequestData(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 diff --git a/lib/jobs/syncjob.h b/lib/jobs/syncjob.h index 830a7c71..b7bfbbb3 100644 --- a/lib/jobs/syncjob.h +++ b/lib/jobs/syncjob.h @@ -15,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/keyverificationsession.cpp b/lib/keyverificationsession.cpp index 3b3b7627..d889b465 100644 --- a/lib/keyverificationsession.cpp +++ b/lib/keyverificationsession.cpp @@ -23,8 +23,10 @@ KeyVerificationSession::KeyVerificationSession(const QString& remoteUserId, cons , m_encrypted(encrypted) , m_remoteSupportedMethods(event.methods()) { - auto timeoutTime = std::min<long int>(event.timestamp() + 600000, QDateTime::currentDateTime().addSecs(120).toMSecsSinceEpoch()); - m_timeout = timeoutTime - QDateTime::currentMSecsSinceEpoch(); + auto timeoutTime = std::min(event.timestamp().addSecs(600), + QDateTime::currentDateTime().addSecs(120)); + m_timeout = + timeoutTime.toMSecsSinceEpoch() - QDateTime::currentMSecsSinceEpoch(); if (m_timeout <= 5000) { return; } diff --git a/lib/logging.h b/lib/logging.h index fc0a4c99..1fafa04b 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -44,12 +44,7 @@ inline QDebug formatJson(QDebug debug_object) //! Suppress full qualification of enums/QFlags when logging inline QDebug terse(QDebug dbg) { - return -#if QT_VERSION < QT_VERSION_CHECK(5, 13, 0) - dbg.setVerbosity(0), dbg; -#else - dbg.verbosity(QDebug::MinimumVerbosity); -#endif + return dbg.verbosity(QDebug::MinimumVerbosity); } inline qint64 profilerMinNsecs() @@ -74,15 +69,13 @@ inline qint64 profilerMinNsecs() */ inline QDebug operator<<(QDebug debug_object, Quotient::QDebugManip qdm) { - return qdm(debug_object); + return qdm(debug_object); // NOLINT(performance-unnecessary-value-param) } -inline QDebug operator<<(QDebug debug_object, const QElapsedTimer& et) +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 index b757bb93..c7547be8 100644 --- a/lib/mxcreply.cpp +++ b/lib/mxcreply.cpp @@ -5,11 +5,10 @@ #include <QtCore/QBuffer> #include "accountregistry.h" -#include "connection.h" #include "room.h" #ifdef Quotient_E2EE_ENABLED -#include "events/encryptedfile.h" +#include "events/filesourceinfo.h" #endif using namespace Quotient; @@ -21,7 +20,7 @@ public: : m_reply(r) {} QNetworkReply* m_reply; - Omittable<EncryptedFile> m_encryptedFile; + Omittable<EncryptedFileMetadata> m_encryptedFile; QIODevice* m_device = nullptr; }; @@ -48,9 +47,9 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) if(!d->m_encryptedFile.has_value()) { d->m_device = d->m_reply; } else { - EncryptedFile file = *d->m_encryptedFile; auto buffer = new QBuffer(this); - buffer->setData(file.decryptFile(d->m_reply->readAll())); + buffer->setData( + decryptFile(d->m_reply->readAll(), *d->m_encryptedFile)); buffer->open(ReadOnly); d->m_device = buffer; } @@ -65,17 +64,13 @@ MxcReply::MxcReply(QNetworkReply* reply, Room* room, const QString &eventId) auto eventIt = room->findInTimeline(eventId); if(eventIt != room->historyEdge()) { auto event = eventIt->viewAs<RoomMessageEvent>(); - d->m_encryptedFile = event->content()->fileInfo()->file; + if (auto* efm = std::get_if<EncryptedFileMetadata>( + &event->content()->fileInfo()->source)) + d->m_encryptedFile = *efm; } #endif } -#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) -#define ERROR_SIGNAL errorOccurred -#else -#define ERROR_SIGNAL error -#endif - MxcReply::MxcReply() : d(ZeroImpl<Private>()) { @@ -87,7 +82,7 @@ MxcReply::MxcReply() setError(QNetworkReply::ProtocolInvalidOperationError, BadRequestPhrase); setFinished(true); - emit ERROR_SIGNAL(QNetworkReply::ProtocolInvalidOperationError); + emit errorOccurred(QNetworkReply::ProtocolInvalidOperationError); emit finished(); }, Qt::QueuedConnection); } diff --git a/lib/networkaccessmanager.cpp b/lib/networkaccessmanager.cpp index f4e7b1af..38ab07cc 100644 --- a/lib/networkaccessmanager.cpp +++ b/lib/networkaccessmanager.cpp @@ -68,24 +68,11 @@ void NetworkAccessManager::clearIgnoredSslErrors() d->ignoredSslErrors.clear(); } -static NetworkAccessManager* createNam() -{ - auto nam = new NetworkAccessManager(); -#if (QT_VERSION < QT_VERSION_CHECK(5, 15, 0)) - // See #109; in newer Qt, bearer management is deprecated altogether - NetworkAccessManager::connect(nam, - &QNetworkAccessManager::networkAccessibleChanged, [nam] { - nam->setNetworkAccessible(QNetworkAccessManager::Accessible); - }); -#endif - return nam; -} - NetworkAccessManager* NetworkAccessManager::instance() { static QThreadStorage<NetworkAccessManager*> storage; if(!storage.hasLocalData()) { - storage.setLocalData(createNam()); + storage.setLocalData(new NetworkAccessManager()); } return storage.localData(); } diff --git a/lib/qt_connection_util.h b/lib/qt_connection_util.h index 86593cc8..ef7f6f80 100644 --- a/lib/qt_connection_util.h +++ b/lib/qt_connection_util.h @@ -9,101 +9,67 @@ 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) { - auto pc = std::make_unique<QMetaObject::Connection>(); - 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); - } - // TODO: get rid of it as soon as Apple Clang gets proper deduction guides - // for std::function<> - // ...or consider using QtPrivate magic used by QObject::connect() - // ...for inspiration, also check a possible std::not_fn implementation - // at https://en.cppreference.com/w/cpp/utility/functional/not_fn - template <typename FnT> - inline auto wrap_in_function(FnT&& f) - { - return typename function_traits<FnT>::function_type(std::forward<FnT>(f)); - } + 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, _impl::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) { #if QT_VERSION_MAJOR >= 6 @@ -111,25 +77,26 @@ inline auto connectSingleShot(SenderT* sender, SignalT signal, Qt::ConnectionType(connType | Qt::SingleShotConnection)); #else - return _impl::connectSingleShot( - sender, signal, context, _impl::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, - _impl::wrap_in_function( - [receiver, slot](const ArgTs&... args) { - (receiver->*slot)(args...); - }), - connType); + // 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 } diff --git a/lib/quotient_common.h b/lib/quotient_common.h index 2b785a39..7fec9274 100644 --- a/lib/quotient_common.h +++ b/lib/quotient_common.h @@ -41,44 +41,8 @@ Q_ENUM_NS_IMPL(Enum) \ Q_FLAG_NS(Flags) -// Apple Clang hasn't caught up with explicit(bool) yet -#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 - -#if QT_VERSION < QT_VERSION_CHECK(5, 14, 0) -// The first line forward-declares the namespace static metaobject with -// QUOTIENT_API so that dynamically linked clients could serialise flag/enum -// values from the namespace; Qt before 5.14 doesn't help with that. The second -// line is needed for moc to do its job on the namespace. -#define QUO_NAMESPACE \ - extern QUOTIENT_API const QMetaObject staticMetaObject; \ - Q_NAMESPACE -#else -// Since Qt 5.14.0, it's all packed in a single macro -#define QUO_NAMESPACE Q_NAMESPACE_EXPORT(QUOTIENT_API) -#endif - namespace Quotient { -QUO_NAMESPACE - -// std::array {} needs explicit template parameters on macOS because -// Apple stdlib doesn't have deduction guides for std::array. C++20 has -// to_array() but that can't be borrowed, this time because of MSVC: -// https://developercommunity.visualstudio.com/t/vc-ice-p1-initc-line-3652-from-stdto-array/1464038 -// Therefore a simpler (but also slightly more wobbly - it resolves the element -// type using std::common_type<>) make_array facility is implemented here. -template <typename... Ts> -constexpr auto make_array(Ts&&... items) -{ - return std::array<std::common_type_t<Ts...>, sizeof...(items)>( - { std::forward<Ts>(items)... }); -} +Q_NAMESPACE_EXPORT(QUOTIENT_API) // TODO: code like this should be generated from the CS API definition @@ -87,7 +51,7 @@ constexpr auto make_array(Ts&&... items) //! These are used for member events. The names here are case-insensitively //! equal to state names used on the wire. //! \sa MemberEventContent, RoomMemberEvent -enum class Membership : unsigned int { +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, @@ -100,9 +64,10 @@ enum class Membership : unsigned int { }; QUO_DECLARE_FLAGS_NS(MembershipMask, Membership) -constexpr auto MembershipStrings = make_array( - // The order MUST be the same as the order in the original enum - "join", "leave", "invite", "knock", "ban"); +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 //! @@ -118,10 +83,10 @@ enum class JoinState : std::underlying_type_t<Membership> { }; QUO_DECLARE_FLAGS_NS(JoinStates, JoinState) -[[maybe_unused]] constexpr auto JoinStateStrings = make_array( +[[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 //! @@ -132,7 +97,7 @@ Q_ENUM_NS(RunningPolicy) //! \brief The result of URI resolution using UriResolver //! \sa UriResolver -enum UriResolveResult : short { +enum UriResolveResult : int8_t { StillResolving = -1, UriResolved = 0, CouldNotResolve, @@ -142,13 +107,19 @@ enum UriResolveResult : short { }; Q_ENUM_NS(UriResolveResult) -enum RoomType { - Space, - Undefined, +enum class RoomType : uint8_t { + Space = 0, + Undefined = 0xFF, }; Q_ENUM_NS(RoomType) -[[maybe_unused]] constexpr auto RoomTypeStrings = make_array("m.space"); +[[maybe_unused]] constexpr std::array RoomTypeStrings { "m.space" }; + +enum class EncryptionType : uint8_t { + MegolmV1AesSha2 = 0, + Undefined = 0xFF, +}; +Q_ENUM_NS(EncryptionType) } // namespace Quotient Q_DECLARE_OPERATORS_FOR_FLAGS(Quotient::MembershipMask) diff --git a/lib/room.cpp b/lib/room.cpp index a423e04f..3ee81dcc 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -12,7 +12,6 @@ #include "avatar.h" #include "connection.h" #include "converters.h" -#include "e2ee/qolmoutboundsession.h" #include "syncdata.h" #include "user.h" #include "eventstats.h" @@ -118,7 +117,7 @@ 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; QHash<QString, Notification> notifications; @@ -219,8 +218,9 @@ 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<StateEventBase>(evtKey.first, + evtKey.second)); qCDebug(STATE) << "A new stub event created for key {" << evtKey.first << evtKey.second << "}"; qCDebug(STATE) << "Stubbed state size:" << stubbedState.size(); @@ -277,10 +277,17 @@ public: * Remove events from the passed container that are already in the timeline */ void dropDuplicateEvents(RoomEvents& events) const; - - Changes setLastReadReceipt(const QString& userId, rev_iter_t newMarker, - ReadReceipt newReceipt = {}, - bool deferStatsUpdate = false); + 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 updateStats(const rev_iter_t& from, const rev_iter_t& to); bool markMessagesAsRead(const rev_iter_t& upToMarker); @@ -340,17 +347,21 @@ public: #ifdef Quotient_E2EE_ENABLED UnorderedMap<QString, QOlmInboundGroupSessionPtr> groupSessions; - + int currentMegolmSessionMessageCount = 0; + //TODO save this to database + unsigned long long currentMegolmSessionCreationTimestamp = 0; QOlmOutboundGroupSessionPtr currentOutboundMegolmSession = nullptr; - bool addInboundGroupSession(QString sessionId, QString sessionKey, const QString& senderId, const QString& olmSessionId) + bool addInboundGroupSession(QString sessionId, QByteArray sessionKey, + const QString& senderId, + const QString& olmSessionId) { - if (groupSessions.find(sessionId) != groupSessions.end()) { + if (groupSessions.contains(sessionId)) { qCWarning(E2EE) << "Inbound Megolm session" << sessionId << "already exists"; return false; } - auto megolmSession = QOlmInboundGroupSession::create(sessionKey.toLatin1()); + auto megolmSession = QOlmInboundGroupSession::create(sessionKey); if (megolmSession->sessionId() != sessionId) { qCWarning(E2EE) << "Session ID mismatch in m.room_key event"; return false; @@ -358,13 +369,12 @@ public: megolmSession->setSenderId(senderId); megolmSession->setOlmSessionId(olmSessionId); qCWarning(E2EE) << "Adding inbound session"; - connection->saveMegolmSession(q, megolmSession.get()); + 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, @@ -375,7 +385,7 @@ public: // qCWarning(E2EE) << "Unable to decrypt event" << eventId // << "The sender's device has not sent us the keys for " // "this message"; - return QString(); + return {}; } auto& senderSession = groupSessionIt->second; if (senderSession->senderId() != senderId) { @@ -383,19 +393,24 @@ public: return {}; } auto decryptResult = senderSession->decrypt(cipher); - if(std::holds_alternative<QOlmError>(decryptResult)) { + if(!decryptResult) { qCWarning(E2EE) << "Unable to decrypt event" << eventId - << "with matching megolm session:" << std::get<QOlmError>(decryptResult); - return QString(); + << "with matching megolm session:" << decryptResult.error(); + return {}; } - const auto& [content, index] = std::get<std::pair<QString, uint32_t>>(decryptResult); - const auto& [recordEventId, ts] = q->connection()->database()->groupSessionIndexRecord(q->id(), senderSession->sessionId(), index); + 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()); + q->connection()->database()->addGroupSessionIndexRecord( + q->id(), senderSession->sessionId(), index, eventId, + timestamp.toMSecsSinceEpoch()); } else { - if ((eventId != recordEventId) || (ts != timestamp.toMSecsSinceEpoch())) { + if ((eventId != recordEventId) + || (ts != timestamp.toMSecsSinceEpoch())) { qCWarning(E2EE) << "Detected a replay attack on event" << eventId; - return QString(); + return {}; } } return content; @@ -403,10 +418,17 @@ public: bool shouldRotateMegolmSession() const { - if (!q->usesEncryption()) { + const auto* encryptionConfig = currentState.get<EncryptionEvent>(); + if (!encryptionConfig || !encryptionConfig->useEncryption()) return false; - } - return currentOutboundMegolmSession->messageCount() >= rotationMessageCount() || currentOutboundMegolmSession->creationTime().addMSecs(rotationInterval()) < QDateTime::currentDateTime(); + + const auto rotationInterval = encryptionConfig->rotationPeriodMs(); + const auto rotationMessageCount = encryptionConfig->rotationPeriodMsgs(); + return currentOutboundMegolmSession->messageCount() + >= rotationMessageCount + || currentOutboundMegolmSession->creationTime().addMSecs( + rotationInterval) + < QDateTime::currentDateTime(); } bool hasValidMegolmSession() const @@ -417,137 +439,45 @@ public: return currentOutboundMegolmSession != nullptr; } - /// Time in milliseconds after which the outgoing megolmsession should be replaced - unsigned int rotationInterval() const - { - if (!q->usesEncryption()) { - return 0; - } - return q->getCurrentState<EncryptionEvent>()->rotationPeriodMs(); - } - - // Number of messages sent by this user after which the outgoing megolm session should be replaced - int rotationMessageCount() const - { - if (!q->usesEncryption()) { - return 0; - } - return q->getCurrentState<EncryptionEvent>()->rotationPeriodMsgs(); - } void createMegolmSession() { - qCDebug(E2EE) << "Creating new outbound megolm session for room " << q->id(); + qCDebug(E2EE) << "Creating new outbound megolm session for room " + << q->objectName(); currentOutboundMegolmSession = QOlmOutboundGroupSession::create(); - connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); const auto sessionKey = currentOutboundMegolmSession->sessionKey(); - if(std::holds_alternative<QOlmError>(sessionKey)) { + if(!sessionKey) { qCWarning(E2EE) << "Failed to load key for new megolm session"; return; } - addInboundGroupSession(currentOutboundMegolmSession->sessionId(), std::get<QByteArray>(sessionKey), q->localUser()->id(), "SELF"_ls); + addInboundGroupSession(currentOutboundMegolmSession->sessionId(), *sessionKey, q->localUser()->id(), "SELF"_ls); } - std::unique_ptr<EncryptedEvent> payloadForUserDevice(User* user, const QString& device, const QByteArray& sessionId, const QByteArray& sessionKey) + QMultiHash<QString, QString> getDevicesWithoutKey() const { - // Noisy but nice for debugging - //qCDebug(E2EE) << "Creating the payload for" << user->id() << device << sessionId << sessionKey.toHex(); - const auto event = makeEvent<RoomKeyEvent>("m.megolm.v1.aes-sha2", q->id(), sessionId, sessionKey, q->localUser()->id()); - QJsonObject payloadJson = event->fullJson(); - payloadJson["recipient"] = user->id(); - payloadJson["sender"] = connection->user()->id(); - QJsonObject recipientObject; - recipientObject["ed25519"] = connection->edKeyForUserDevice(user->id(), device); - payloadJson["recipient_keys"] = recipientObject; - QJsonObject senderObject; - senderObject["ed25519"] = QString(connection->olmAccount()->identityKeys().ed25519); - payloadJson["keys"] = senderObject; - payloadJson["sender_device"] = connection->deviceId(); - auto cipherText = connection->olmEncryptMessage(user, device, QJsonDocument(payloadJson).toJson(QJsonDocument::Compact)); - QJsonObject encrypted; - encrypted[connection->curveKeyForUserDevice(user->id(), device)] = QJsonObject{{"type", cipherText.first}, {"body", QString(cipherText.second)}}; - - return makeEvent<EncryptedEvent>(encrypted, connection->olmAccount()->identityKeys().curve25519); - } - - QHash<User*, QStringList> getDevicesWithoutKey() const - { - QHash<User*, QStringList> devices; - auto rawDevices = q->connection()->database()->devicesWithoutKey(q, QString(currentOutboundMegolmSession->sessionId())); - for (const auto& user : rawDevices.keys()) { - devices[q->connection()->user(user)] = rawDevices[user]; - } - return devices; - } + QMultiHash<QString, QString> devices; + for (const auto& user : q->users()) + for (const auto& deviceId : connection->devicesForUser(user->id())) + devices.insert(user->id(), deviceId); - void sendRoomKeyToDevices(const QByteArray& sessionId, const QByteArray& sessionKey, const QHash<User*, QStringList> devices, int index) - { - qCDebug(E2EE) << "Sending room key to devices" << sessionId, sessionKey.toHex(); - QHash<QString, QHash<QString, QString>> hash; - for (const auto& user : devices.keys()) { - QHash<QString, QString> u; - for(const auto &device : devices[user]) { - if (!connection->hasOlmSession(user, device)) { - u[device] = "signed_curve25519"_ls; - qCDebug(E2EE) << "Adding" << user << device << "to keys to claim"; - } - } - if (!u.isEmpty()) { - hash[user->id()] = u; - } - } - if (hash.isEmpty()) { - return; - } - auto job = connection->callApi<ClaimKeysJob>(hash); - connect(job, &BaseJob::success, q, [job, this, sessionId, sessionKey, devices, index](){ - Connection::UsersToDevicesToEvents usersToDevicesToEvents; - const auto data = job->jsonData(); - for(const auto &user : devices.keys()) { - for(const auto &device : devices[user]) { - const auto recipientCurveKey = connection->curveKeyForUserDevice(user->id(), device); - if (!connection->hasOlmSession(user, device)) { - qCDebug(E2EE) << "Creating a new session for" << user << device; - if(data["one_time_keys"][user->id()][device].toObject().isEmpty()) { - qWarning() << "No one time key for" << user << device; - continue; - } - const auto keyId = data["one_time_keys"][user->id()][device].toObject().keys()[0]; - const auto oneTimeKey = data["one_time_keys"][user->id()][device][keyId]["key"].toString(); - const auto signature = data["one_time_keys"][user->id()][device][keyId]["signatures"][user->id()][QStringLiteral("ed25519:") + device].toString().toLatin1(); - auto signedData = data["one_time_keys"][user->id()][device][keyId].toObject(); - signedData.remove("unsigned"); - signedData.remove("signatures"); - auto signatureMatch = QOlmUtility().ed25519Verify(connection->edKeyForUserDevice(user->id(), device).toLatin1(), QJsonDocument(signedData).toJson(QJsonDocument::Compact), signature); - if (std::holds_alternative<QOlmError>(signatureMatch)) { - qCWarning(E2EE) << "Failed to verify one-time-key signature for" << user->id() << device << ". Skipping this device."; - continue; - } else { - } - connection->createOlmSession(recipientCurveKey, oneTimeKey); - } - usersToDevicesToEvents[user->id()][device] = payloadForUserDevice(user, device, sessionId, sessionKey); - } - } - if (!usersToDevicesToEvents.empty()) { - connection->sendToDevices("m.room.encrypted", usersToDevicesToEvents); - connection->database()->setDevicesReceivedKey(q->id(), devices, sessionId, index); - } - }); + return connection->database()->devicesWithoutKey( + id, devices, currentOutboundMegolmSession->sessionId()); } - void sendMegolmSession(const QHash<User *, QStringList>& devices) { + void sendMegolmSession(const QMultiHash<QString, QString>& devices) const { // Save the session to this device const auto sessionId = currentOutboundMegolmSession->sessionId(); - const auto _sessionKey = currentOutboundMegolmSession->sessionKey(); - if(std::holds_alternative<QOlmError>(_sessionKey)) { + const auto sessionKey = currentOutboundMegolmSession->sessionKey(); + if(!sessionKey) { qCWarning(E2EE) << "Error loading session key"; return; } - const auto sessionKey = std::get<QByteArray>(_sessionKey); - const auto senderKey = q->connection()->olmAccount()->identityKeys().curve25519; // Send the session to other people - sendRoomKeyToDevices(sessionId, sessionKey, devices, currentOutboundMegolmSession->sessionMessageIndex()); + connection->sendSessionKeyToDevices( + id, sessionId, *sessionKey, devices, + currentOutboundMegolmSession->sessionMessageIndex()); } #endif // Quotient_E2EE_ENABLED @@ -569,11 +499,6 @@ 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); @@ -584,7 +509,8 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) } }); d->groupSessions = connection->loadRoomMegolmSessions(this); - d->currentOutboundMegolmSession = connection->loadCurrentOutboundMegolmSession(this); + d->currentOutboundMegolmSession = + connection->loadCurrentOutboundMegolmSession(this->id()); if (d->shouldRotateMegolmSession()) { d->currentOutboundMegolmSession = nullptr; } @@ -592,7 +518,9 @@ Room::Room(Connection* connection, QString id, JoinState initialJoinState) if (!usesEncryption()) { return; } - d->currentOutboundMegolmSession = nullptr; + if (d->hasValidMegolmSession()) { + d->createMegolmSession(); + } qCDebug(E2EE) << "Invalidating current megolm session because user left"; }); @@ -779,10 +707,9 @@ void Room::setJoinState(JoinState state) emit joinStateChanged(oldState, state); } -Room::Changes Room::Private::setLastReadReceipt(const QString& userId, - rev_iter_t newMarker, - ReadReceipt newReceipt, - bool deferStatsUpdate) +Omittable<QString> Room::Private::setLastReadReceipt(const QString& userId, + rev_iter_t newMarker, + ReadReceipt newReceipt) { if (newMarker == historyEdge() && !newReceipt.eventId.isEmpty()) newMarker = q->findInTimeline(newReceipt.eventId); @@ -796,7 +723,7 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, // eagerMarker is now just after the desired event for newMarker if (eagerMarker != newMarker.base()) { newMarker = rev_iter_t(eagerMarker); - qCDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId + qDebug(EPHEMERAL) << "Auto-promoted read receipt for" << userId << "to" << *newMarker; } // Fill newReceipt with the event (and, if needed, timestamp) from @@ -810,14 +737,19 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, 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 Change::None; + return {}; // Finally make the change - Changes changes = Change::Other; auto oldEventReadUsersIt = eventIdReadUsers.find(prevEventId); // clazy:exclude=detaching-member if (oldEventReadUsersIt != eventIdReadUsers.end()) { @@ -829,7 +761,7 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, storedReceipt = move(newReceipt); { - auto dbg = qDebug(EPHEMERAL); // This trick needs qDebug, not qCDebug + 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; @@ -837,25 +769,37 @@ Room::Changes Room::Private::setLastReadReceipt(const QString& userId, dbg << *newMarker; } - // TODO: use Room::member() when it becomes a thing and only emit signals - // for actual members, not just any user - const auto member = q->user(userId); - Q_ASSERT(member != nullptr); - if (isLocalUser(member) && !deferStatsUpdate) { - if (unreadStats.updateOnMarkerMove(q, q->findInTimeline(prevEventId), + // 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)) + 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)) { - qCDebug(MESSAGES) + 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(member); - // TODO: remove in 0.8 - if (!isLocalUser(member)) - emit q->readMarkerForUserMoved(member, prevEventId, - storedReceipt.eventId); + emit q->lastReadEventChanged({ connection->userId() }); return changes; } @@ -870,7 +814,7 @@ Room::Changes Room::Private::updateStats(const rev_iter_t& from, Changes changes = Change::None; // Correct the read receipt to never be behind the fully read marker if (readReceiptMarker > fullyReadMarker - && setLastReadReceipt(connection->userId(), fullyReadMarker, {}, true)) { + && setLocalLastReadReceipt(fullyReadMarker, {}, true)) { changes |= Change::Other; readReceiptMarker = q->localReadReceiptMarker(); qCInfo(MESSAGES) << "The local m.read receipt was behind m.fully_read " @@ -968,7 +912,7 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) QT_IGNORE_DEPRECATIONS(Changes changes = Change::ReadMarker|Change::Other;) if (const auto rm = q->fullyReadMarker(); rm != historyEdge()) { // Pull read receipt if it's behind, and update statistics - changes |= setLastReadReceipt(connection->userId(), rm); + changes |= setLocalLastReadReceipt(rm); if (partiallyReadStats.updateOnMarkerMove(q, prevReadMarker, rm)) { changes |= Change::PartiallyReadStats; qCDebug(MESSAGES) @@ -986,9 +930,8 @@ Room::Changes Room::Private::setFullyReadMarker(const QString& eventId) void Room::setReadReceipt(const QString& atEventId) { - if (const auto changes = d->setLastReadReceipt(localUser()->id(), - historyEdge(), - { atEventId })) { + if (const auto changes = + d->setLocalLastReadReceipt(historyEdge(), { atEventId })) { connection()->callApi<PostReceiptJob>(BackgroundRequest, id(), QStringLiteral("m.read"), QUrl::toPercentEncoding(atEventId)); @@ -1517,7 +1460,7 @@ QUrl Room::urlToThumbnail(const QString& eventId) const auto* thumbnail = event->content()->thumbnailInfo(); Q_ASSERT(thumbnail != nullptr); return connection()->getUrlForApi<MediaThumbnailJob>( - thumbnail->url, thumbnail->imageSize); + thumbnail->url(), thumbnail->imageSize); } qCDebug(MAIN) << "Event" << eventId << "has no thumbnail"; return {}; @@ -1528,7 +1471,7 @@ QUrl Room::urlToDownload(const QString& eventId) const if (auto* event = d->getEventWithFile(eventId)) { auto* fileInfo = event->content()->fileInfo(); Q_ASSERT(fileInfo != nullptr); - return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url); + return connection()->getUrlForApi<DownloadFileJob>(fileInfo->url()); } return {}; } @@ -1600,7 +1543,7 @@ QStringList Room::safeMemberNames() const { QStringList res; res.reserve(d->membersMap.size()); - for (auto u: std::as_const(d->membersMap)) + for (const auto* u: std::as_const(d->membersMap)) res.append(safeMemberName(u->id())); return res; @@ -1610,7 +1553,7 @@ QStringList Room::htmlSafeMemberNames() const { QStringList res; res.reserve(d->membersMap.size()); - for (auto u: std::as_const(d->membersMap)) + for (const auto* u: std::as_const(d->membersMap)) res.append(htmlSafeMemberName(u->id())); return res; @@ -1649,9 +1592,9 @@ RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) return {}; } QString decrypted = d->groupSessionDecryptMessage( - encryptedEvent.ciphertext(), encryptedEvent.senderKey(), - encryptedEvent.sessionId(), encryptedEvent.id(), - encryptedEvent.originTimestamp(), encryptedEvent.senderId()); + encryptedEvent.ciphertext(), encryptedEvent.sessionId(), + encryptedEvent.id(), encryptedEvent.originTimestamp(), + encryptedEvent.senderId()); if (decrypted.isEmpty()) { // qCWarning(E2EE) << "Encrypted message is empty"; return {}; @@ -1680,7 +1623,8 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, << roomKeyEvent.algorithm() << "in m.room_key event"; } if (d->addInboundGroupSession(roomKeyEvent.sessionId(), - roomKeyEvent.sessionKey(), senderId, olmSessionId)) { + roomKeyEvent.sessionKey(), senderId, + olmSessionId)) { qCWarning(E2EE) << "added new inboundGroupSession:" << d->groupSessions.size(); auto undecryptedEvents = d->undecryptedEvents[roomKeyEvent.sessionId()]; @@ -1690,8 +1634,7 @@ void Room::handleRoomKeyEvent(const RoomKeyEvent& roomKeyEvent, continue; auto& ti = d->timeline[Timeline::size_type(*pIdx - minTimelineIndex())]; if (auto encryptedEvent = ti.viewAs<EncryptedEvent>()) { - auto decrypted = decryptMessage(*encryptedEvent); - if(decrypted) { + if (auto decrypted = decryptMessage(*encryptedEvent)) { // The reference will survive the pointer being moved auto& decryptedEvent = *decrypted; auto oldEvent = ti.replaceEvent(std::move(decrypted)); @@ -1986,6 +1929,9 @@ Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, 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); @@ -2011,6 +1957,9 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) emit namesChanged(this); d->postprocessChanges(roomChanges, !fromCache); + if (firstUpdate) + emit baseStateLoaded(); + qCDebug(MAIN) << "--- Finished updating room" << id() << "/" << objectName(); } void Room::Private::postprocessChanges(Changes changes, bool saveState) @@ -2036,11 +1985,8 @@ void Room::Private::postprocessChanges(Changes changes, bool saveState) if (changes & Change::Highlights) emit q->highlightCountChanged(); - qCDebug(MAIN) << terse << changes << "= hex" << -#if QT_VERSION >= QT_VERSION_CHECK(5, 14, 0) - Qt:: -#endif - hex << uint(changes) << "in" << q->objectName(); + qCDebug(MAIN) << terse << changes << "= hex" << Qt::hex << uint(changes) + << "in" << q->objectName(); emit q->changed(changes); if (saveState) connection->saveRoomState(q); @@ -2076,6 +2022,7 @@ 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 @@ -2085,17 +2032,17 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) if (!hasValidMegolmSession() || shouldRotateMegolmSession()) { createMegolmSession(); } - const auto devicesWithoutKey = getDevicesWithoutKey(); - sendMegolmSession(devicesWithoutKey); + sendMegolmSession(getDevicesWithoutKey()); const auto encrypted = currentOutboundMegolmSession->encrypt(QJsonDocument(pEvent->fullJson()).toJson()); currentOutboundMegolmSession->setMessageCount(currentOutboundMegolmSession->messageCount() + 1); - connection->saveCurrentOutboundMegolmSession(q, currentOutboundMegolmSession); - if(std::holds_alternative<QOlmError>(encrypted)) { - qWarning(E2EE) << "Error encrypting message" << std::get<QOlmError>(encrypted); + connection->saveCurrentOutboundMegolmSession( + id, *currentOutboundMegolmSession); + if(!encrypted) { + qWarning(E2EE) << "Error encrypting message" << encrypted.error(); return {}; } - auto encryptedEvent = new EncryptedEvent(std::get<QByteArray>(encrypted), q->connection()->olmAccount()->identityKeys().curve25519, q->connection()->deviceId(), currentOutboundMegolmSession->sessionId()); + 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()); @@ -2103,7 +2050,7 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) 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; + _event = encryptedEvent.get(); #endif } @@ -2114,17 +2061,18 @@ QString Room::Private::doSendEvent(const RoomEvent* pEvent) 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(); 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, _event] { + 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) { @@ -2132,13 +2080,10 @@ 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()); - if (q->usesEncryption()){ - delete _event; - } }); } else onEventSendingFailure(txnId); @@ -2193,11 +2138,9 @@ QString Room::retryMessage(const QString& txnId) return d->doSendEvent(it->event()); } -// Lambda defers actual tr() invocation to the moment when translations are -// initialised -const auto FileTransferCancelledMsg = [] { - return Room::tr("File transfer cancelled"); -}; +// 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) { @@ -2262,28 +2205,26 @@ QString Room::Private::doPostFile(RoomEventPtr&& msgEvent, const QUrl& localUrl) // Below, the upload job is used as a context object to clean up connections const auto& transferJob = fileTransfers.value(txnId).job; connect(q, &Room::fileTransferCompleted, transferJob, - [this, txnId](const QString& tId, const QUrl&, const QUrl& mxcUri, Omittable<EncryptedFile> encryptedFile) { - if (tId != txnId) - return; + [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(mxcUri); - if (encryptedFile) { - it->setEncryptedFile(*encryptedFile); - } - emit q->pendingEventChanged( - int(it - unsyncedEvents.begin())); - doSendEvent(it->get()); - } else { - // Normally in this situation we should instruct - // the media server to delete the file; alas, there's no - // API specced for that. - qCWarning(MAIN) << "File uploaded to" << mxcUri - << "but the event referring to it was " - "cancelled"; - } - }); + 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) @@ -2309,13 +2250,13 @@ QString Room::postFile(const QString& plainText, Q_ASSERT(content != nullptr && content->fileInfo() != nullptr); const auto* const fileInfo = content->fileInfo(); Q_ASSERT(fileInfo != nullptr); - QFileInfo localFile { fileInfo->url.toLocalFile() }; + QFileInfo localFile { fileInfo->url().toLocalFile() }; Q_ASSERT(localFile.isFile()); return d->doPostFile( makeEvent<RoomMessageEvent>( plainText, RoomMessageEvent::rawMsgTypeForFile(localFile), content), - fileInfo->url); + fileInfo->url()); } #if QT_VERSION_MAJOR < 6 @@ -2343,8 +2284,7 @@ QString Room::postJson(const QString& matrixType, SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) { - return d->requestSetState(evt.matrixType(), evt.stateKey(), - evt.contentJson()); + return setState(evt.matrixType(), evt.stateKey(), evt.contentJson()); } SetRoomStateWithKeyJob* Room::setState(const QString& evtType, @@ -2431,11 +2371,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) @@ -2450,15 +2391,18 @@ void Room::hangupCall(const QString& callId) d->sendEvent<CallHangupEvent>(callId); } -void Room::getPreviousContent(int limit, const QString &filter) { d->getPreviousContent(limit, filter); } +void Room::getPreviousContent(int limit, const QString& filter) +{ + d->getPreviousContent(limit, filter); +} void Room::Private::getPreviousContent(int limit, const QString &filter) { if (isJobPending(eventsHistoryJob)) return; - eventsHistoryJob = - connection->callApi<GetRoomEventsJob>(id, prevBatch, "b", "", limit, filter); + eventsHistoryJob = connection->callApi<GetRoomEventsJob>(id, "b", prevBatch, + "", limit, filter); emit q->eventsHistoryJobChanged(); connect(eventsHistoryJob, &BaseJob::success, q, [this] { prevBatch = eventsHistoryJob->end(); @@ -2506,18 +2450,18 @@ 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(); - Omittable<EncryptedFile> encryptedFile = std::nullopt; + FileSourceInfo fileMetadata; #ifdef Quotient_E2EE_ENABLED QTemporaryFile tempFile; if (usesEncryption()) { tempFile.open(); QFile file(localFilename.toLocalFile()); file.open(QFile::ReadOnly); - auto [e, data] = EncryptedFile::encryptFile(file.readAll()); + QByteArray data; + std::tie(fileMetadata, data) = encryptFile(file.readAll()); tempFile.write(data); tempFile.close(); fileName = QFileInfo(tempFile).absoluteFilePath(); - encryptedFile = e; } #endif auto job = connection()->uploadFile(fileName, overrideContentType); @@ -2528,16 +2472,13 @@ void Room::uploadFile(const QString& id, const QUrl& localFilename, d->fileTransfers[id].update(sent, total); emit fileTransferProgress(id, sent, total); }); - connect(job, &BaseJob::success, this, [this, id, localFilename, job, encryptedFile] { - d->fileTransfers[id].status = FileTransferInfo::Completed; - if (encryptedFile) { - auto file = *encryptedFile; - file.url = QUrl(job->contentUri()); - emit fileTransferCompleted(id, localFilename, QUrl(job->contentUri()), file); - } else { - emit fileTransferCompleted(id, localFilename, QUrl(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); @@ -2570,11 +2511,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, "---"); @@ -2584,9 +2525,9 @@ void Room::downloadFile(const QString& eventId, const QUrl& localFilename) } DownloadFileJob *job = nullptr; #ifdef Quotient_E2EE_ENABLED - if(fileInfo->file.has_value()) { - auto file = *fileInfo->file; - job = connection()->downloadFile(fileUrl, file, filePath); + if (auto* fileMetadata = + std::get_if<EncryptedFileMetadata>(&fileInfo->source)) { + job = connection()->downloadFile(fileUrl, *fileMetadata, filePath); } else { #endif job = connection()->downloadFile(fileUrl, filePath); @@ -2609,6 +2550,7 @@ 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); } @@ -2652,6 +2594,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 @@ -2679,10 +2641,11 @@ RoomEventPtr makeRedacted(const RoomEvent& target, { QStringLiteral("ban"), QStringLiteral("events"), QStringLiteral("events_default"), QStringLiteral("kick"), QStringLiteral("redact"), QStringLiteral("state_default"), - QStringLiteral("users"), QStringLiteral("users_default") } } - // , { RoomJoinRules::typeId(), { QStringLiteral("join_rule") } } - // , { RoomHistoryVisibility::typeId(), - // { QStringLiteral("history_visibility") } } + QStringLiteral("users"), QStringLiteral("users_default") } }, + // 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())) @@ -2754,7 +2717,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } if (const auto* reaction = eventCast<ReactionEvent>(oldEvent)) { const auto& targetEvtId = reaction->relation().eventId; - const QPair lookupKey { targetEvtId, EventRelation::AnnotationType }; + const std::pair lookupKey { targetEvtId, EventRelation::AnnotationType }; if (relations.contains(lookupKey)) { relations[lookupKey].removeOne(reaction); emit q->updatedEvent(targetEvtId); @@ -2842,23 +2805,11 @@ Room::Changes Room::Private::addNewMessageEvents(RoomEvents&& events) if (events.empty()) return Change::None; + decryptIncomingEvents(events); + QElapsedTimer et; et.start(); -#ifdef Quotient_E2EE_ENABLED - for(long unsigned int i = 0; i < events.size(); i++) { - if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) { - auto decrypted = q->decryptMessage(*encrypted); - if(decrypted) { - auto oldEvent = std::exchange(events[i], std::move(decrypted)); - events[i]->setOriginalEvent(std::move(oldEvent)); - } else { - undecryptedEvents[encrypted->sessionId()] += encrypted->id(); - } - } - } -#endif - { // Pre-process redactions and edits so that events that get // redacted/replaced in the same batch landed in the timeline already @@ -3002,30 +2953,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; - Changes changes {}; - -#ifdef Quotient_E2EE_ENABLED - for(long unsigned int i = 0; i < events.size(); i++) { - if(auto* encrypted = eventCast<EncryptedEvent>(events[i])) { - auto decrypted = q->decryptMessage(*encrypted); - if(decrypted) { - auto oldEvent = std::exchange(events[i], std::move(decrypted)); - events[i]->setOriginalEvent(std::move(oldEvent)); - } else { - undecryptedEvents[encrypted->sessionId()] += encrypted->id(); - } - } - } -#endif + 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 @@ -3142,7 +3080,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return false; } if (oldEncEvt - && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { + && oldEncEvt->encryption() != EncryptionType::Undefined) { qCWarning(STATE) << "The room is already encrypted but a new" " room encryption event arrived - ignoring"; return false; @@ -3283,58 +3221,66 @@ Room::Changes Room::processEphemeralEvent(EventPtr&& event) Changes changes {}; QElapsedTimer et; et.start(); - if (auto* evt = eventCast<TypingEvent>(event)) { - d->usersTyping.clear(); - d->usersTyping.reserve(evt->users().size()); // Assume all are members - for (const auto& userId : evt->users()) - if (isMember(userId)) - d->usersTyping.append(user(userId)); - - if (evt->users().size() > 3 || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) - << "Processing typing events from" << evt->users().size() - << "user(s) in" << objectName() << "took" << et; - emit typingChanged(); - } - if (auto* evt = eventCast<ReceiptEvent>(event)) { - int totalReceipts = 0; - const auto& eventsWithReceipts = evt->eventsWithReceipts(); - for (const auto& p : eventsWithReceipts) { - totalReceipts += p.receipts.size(); - const auto newMarker = findInTimeline(p.evtId); - if (newMarker == historyEdge()) - qCDebug(EPHEMERAL) - << "Event" << p.evtId - << "is not found; saving read receipt(s) anyway"; - // If the event is not found (most likely, because it's too old and - // hasn't been fetched from the server yet) but there is a previous - // marker for a user, keep the previous marker because read receipts - // are not supposed to move backwards. Otherwise, blindly store - // the event id for this user and update the read marker when/if - // the event is fetched later on. - const auto updatedCount = std::count_if( - p.receipts.cbegin(), p.receipts.cend(), - [this, &changes, &newMarker, &evtId = p.evtId](const auto& r) { - const auto change = - d->setLastReadReceipt(r.userId, newMarker, - { evtId, r.timestamp }); - changes |= change; - return change & Change::Any; - }); - - if (p.receipts.size() > 1) - qCDebug(EPHEMERAL) << p.evtId << "marked as read for" - << updatedCount << "user(s)"; - if (updatedCount < p.receipts.size()) - qCDebug(EPHEMERAL) << p.receipts.size() - updatedCount - << "receipts were skipped"; - } - if (eventsWithReceipts.size() > 3 || totalReceipts > 10 - || et.nsecsElapsed() >= profilerMinNsecs()) - qCDebug(PROFILER) << "Processing" << totalReceipts - << "receipt(s) on" << eventsWithReceipts.size() - << "event(s) in" << objectName() << "took" << et; - } + 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 (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; } @@ -3444,7 +3390,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 @@ -3587,5 +3533,5 @@ void Room::activateEncryption() qCWarning(E2EE) << "Room" << objectName() << "is already encrypted"; return; } - setState<EncryptionEvent>(EncryptionEventContent::MegolmV1AesSha2); + setState<EncryptionEvent>(EncryptionType::MegolmV1AesSha2); } @@ -758,7 +758,8 @@ public: [[deprecated("Use currentState().get() instead; " "make sure to check its result for nullptrs")]] // const Quotient::StateEventBase* - getCurrentState(const QString& evtType, const QString& stateKey = {}) const; + 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 @@ -870,8 +871,9 @@ public Q_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); @@ -971,9 +973,9 @@ Q_SIGNALS: void displayedChanged(bool displayed); void firstDisplayedEventChanged(); void lastDisplayedEventChanged(); - //! The event that m.read receipt points to has changed + //! The event the m.read receipt points to has changed for the listed users //! \sa lastReadReceipt - void lastReadEventChanged(Quotient::User* user); + void lastReadEventChanged(QVector<QString> userIds); void fullyReadMarkerMoved(QString fromEventId, QString toEventId); //! \deprecated since 0.7 - use fullyReadMarkerMoved void readMarkerMoved(QString fromEventId, QString toEventId); @@ -997,7 +999,8 @@ Q_SIGNALS: void newFileTransfer(QString id, QUrl localFile); void fileTransferProgress(QString id, qint64 progress, qint64 total); - void fileTransferCompleted(QString id, QUrl localFile, QUrl mxcUrl, Omittable<EncryptedFile> encryptedFile = std::nullopt); + void fileTransferCompleted(QString id, QUrl localFile, + FileSourceInfo fileMetadata); void fileTransferFailed(QString id, QString errorMessage = {}); // fileTransferCancelled() is no more here; use fileTransferFailed() and // check the transfer status instead diff --git a/lib/roomstateview.h b/lib/roomstateview.h index cab69ae3..29cce00e 100644 --- a/lib/roomstateview.h +++ b/lib/roomstateview.h @@ -11,7 +11,8 @@ namespace Quotient { class Room; -class RoomStateView : private QHash<StateEventKey, const StateEventBase*> { +class QUOTIENT_API RoomStateView + : private QHash<StateEventKey, const StateEventBase*> { Q_GADGET public: const QHash<StateEventKey, const StateEventBase*>& events() const diff --git a/lib/syncdata.cpp b/lib/syncdata.cpp index 78957cbe..93416bc4 100644 --- a/lib/syncdata.cpp +++ b/lib/syncdata.cpp @@ -18,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) @@ -142,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) { @@ -150,18 +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); } +DevicesList SyncData::takeDevicesList() { return std::move(devicesList); } QJsonObject SyncData::loadJson(const QString& fileName) { @@ -179,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"; diff --git a/lib/syncdata.h b/lib/syncdata.h index 6b70140d..9358ec8f 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -98,15 +98,15 @@ 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(); - DevicesList&& takeDevicesList(); + SyncDataList takeRoomData(); + DevicesList takeDevicesList(); QString nextBatch() const { return nextBatch_; } diff --git a/lib/uri.cpp b/lib/uri.cpp index 6b7d1d20..91751df0 100644 --- a/lib/uri.cpp +++ b/lib/uri.cpp @@ -171,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"); diff --git a/lib/util.cpp b/lib/util.cpp index 03ebf325..359b2959 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -135,3 +135,12 @@ int Quotient::patchVersion() { return Quotient_VERSION_PATCH; } + +bool Quotient::encryptionSupported() +{ +#ifdef Quotient_E2EE_ENABLED + return true; +#else + return false; +#endif +} @@ -37,6 +37,31 @@ static_assert(false, "Use Q_DISABLE_MOVE instead; Quotient enables it across all 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, ...> template <typename T> @@ -93,8 +118,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) @@ -110,8 +135,8 @@ inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, //! 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> -using ImplPtr = std::unique_ptr<ImplType, void (*)(ImplType*)>; +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 @@ -131,20 +156,42 @@ using ImplPtr = std::unique_ptr<ImplType, void (*)(ImplType*)>; //! //! 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 DeleterType = void (*)(ImplType*), - typename... ArgTs> -inline ImplPtr<ImplType> makeImpl(ArgTs&&... args) +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> { new ImplType(std::forward<ArgTs>(args)...), - [](ImplType* impl) { delete impl; } }; + return ImplPtr<ImplType, TypeToDelete> { from, [](TypeToDelete* impl) { + delete impl; + } }; } -template <typename ImplType> -const inline ImplPtr<ImplType> ZeroImpl() +template <typename ImplType, typename TypeToDelete = ImplType> +constexpr ImplPtr<ImplType, TypeToDelete> ZeroImpl() { - return { nullptr, [](ImplType*) { /* nullptr doesn't need deletion */ } }; + 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 */ QUOTIENT_API void linkifyUrls(QString& htmlEscapedText); @@ -183,4 +230,5 @@ QUOTIENT_API QString versionString(); QUOTIENT_API int majorVersion(); QUOTIENT_API int minorVersion(); QUOTIENT_API int patchVersion(); +QUOTIENT_API bool encryptionSupported(); } // namespace Quotient |