diff options
author | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2019-07-09 11:49:05 +0900 |
---|---|---|
committer | Kitsune Ral <Kitsune-Ral@users.sf.net> | 2019-07-09 11:49:05 +0900 |
commit | 31e28e2a99e6815da407d201e7287423a4956138 (patch) | |
tree | 049f3b156ad2cca3f328d163c9267ae90d984485 /lib | |
parent | b5dd30189df0d7515116b2abac1f93fc0f8a1989 (diff) | |
parent | 651478c1681ba6f93e22c20328a048dbbc263ffe (diff) | |
download | libquotient-31e28e2a99e6815da407d201e7287423a4956138.tar.gz libquotient-31e28e2a99e6815da407d201e7287423a4956138.zip |
Merge branch 'master' into use-clang-format
Diffstat (limited to 'lib')
-rw-r--r-- | lib/connection.cpp | 107 | ||||
-rw-r--r-- | lib/connection.h | 14 | ||||
-rw-r--r-- | lib/encryptionmanager.cpp | 228 | ||||
-rw-r--r-- | lib/encryptionmanager.h | 34 | ||||
-rw-r--r-- | lib/events/encryptionevent.cpp | 54 | ||||
-rw-r--r-- | lib/events/encryptionevent.h | 81 | ||||
-rw-r--r-- | lib/events/event.h | 6 | ||||
-rw-r--r-- | lib/events/eventcontent.h | 10 | ||||
-rw-r--r-- | lib/events/eventloader.h | 20 | ||||
-rw-r--r-- | lib/events/roomevent.cpp | 12 | ||||
-rw-r--r-- | lib/events/roomevent.h | 3 | ||||
-rw-r--r-- | lib/events/roommemberevent.h | 12 | ||||
-rw-r--r-- | lib/events/simplestateevents.h | 25 | ||||
-rw-r--r-- | lib/events/stateevent.cpp | 8 | ||||
-rw-r--r-- | lib/events/stateevent.h | 22 | ||||
-rw-r--r-- | lib/jobs/basejob.cpp | 147 | ||||
-rw-r--r-- | lib/jobs/basejob.h | 18 | ||||
-rw-r--r-- | lib/joinstate.h | 2 | ||||
-rw-r--r-- | lib/logging.cpp | 12 | ||||
-rw-r--r-- | lib/room.cpp | 186 | ||||
-rw-r--r-- | lib/room.h | 42 | ||||
-rw-r--r-- | lib/settings.cpp | 27 | ||||
-rw-r--r-- | lib/settings.h | 6 | ||||
-rw-r--r-- | lib/syncdata.h | 2 | ||||
-rw-r--r-- | lib/user.cpp | 4 | ||||
-rw-r--r-- | lib/util.cpp | 17 | ||||
-rw-r--r-- | lib/util.h | 7 |
27 files changed, 880 insertions, 226 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index 43681d12..52a693f7 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -19,6 +19,7 @@ #include "connection.h" #include "connectiondata.h" +#include "encryptionmanager.h" #include "room.h" #include "settings.h" #include "user.h" @@ -83,8 +84,9 @@ public: // separately; specifically, we should keep objects for Invite and // Leave state of the same room if the two happen to co-exist. QHash<QPair<QString, bool>, Room*> roomMap; - // Mapping from aliases to room ids, as per the last sync - QHash<QString, QString> roomAliasMap; + /// Mapping from serverparts to alias/room id mappings, + /// as of the last sync + QHash<QString, QHash<QString, QString>> roomAliasMap; QVector<QString> roomIdsToForget; QVector<Room*> firstTimeRooms; QVector<QString> pendingStateRoomIds; @@ -103,6 +105,8 @@ public: GetCapabilitiesJob* capabilitiesJob = nullptr; GetCapabilitiesJob::Capabilities capabilities; + QScopedPointer<EncryptionManager> encryptionManager; + SyncJob* syncJob = nullptr; bool cacheState = true; @@ -113,6 +117,7 @@ public: void connectWithToken(const QString& user, const QString& accessToken, const QString& deviceId); + void removeRoom(const QString& roomId); template <typename EventT> EventT* unpackAccountData() const @@ -160,22 +165,11 @@ Connection::~Connection() stopSync(); } -void Connection::resolveServer(const QString& mxidOrDomain) +void Connection::resolveServer(const QString& mxid) { - // At this point we may have something as complex as - // @username:[IPv6:address]:port, or as simple as a plain domain name. - - // Try to parse as an FQID; if there's no @ part, assume it's a domain name. - QRegularExpression parser( - "^(@.+?:)?" // Optional username (allow everything for compatibility) - "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address - "(:\\d{1,5})?$", // Optional port - QRegularExpression::UseUnicodePropertiesOption); // Because asian digits - auto match = parser.match(mxidOrDomain); - - QUrl maybeBaseUrl = QUrl::fromUserInput(match.captured(2)); + auto maybeBaseUrl = QUrl::fromUserInput(serverPart(mxid)); maybeBaseUrl.setScheme("https"); // Instead of the Qt-default "http" - if (!match.hasMatch() || !maybeBaseUrl.isValid()) { + if (maybeBaseUrl.isEmpty() || !maybeBaseUrl.isValid()) { emit resolveError(tr("%1 is not a valid homeserver address") .arg(maybeBaseUrl.toString())); return; @@ -234,6 +228,17 @@ void Connection::doConnectToServer(const QString& user, const QString& password, connect(loginJob, &BaseJob::success, this, [this, loginJob] { d->connectWithToken(loginJob->userId(), loginJob->accessToken(), loginJob->deviceId()); + + AccountSettings accountSettings(loginJob->userId()); + d->encryptionManager.reset( + new EncryptionManager(accountSettings.encryptionAccountPickle())); + if (accountSettings.encryptionAccountPickle().isEmpty()) { + accountSettings.setEncryptionAccountPickle( + d->encryptionManager->olmAccountPickle()); + } + + d->encryptionManager->uploadIdentityKeys(this); + d->encryptionManager->uploadOneTimeKeys(this); }); connect(loginJob, &BaseJob::failure, this, [this, loginJob] { emit loginError(loginJob->errorString(), loginJob->rawDataSample()); @@ -763,25 +768,33 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id) room = d->roomMap.value({ id, true }); if (room && room->joinState() != JoinState::Leave) { auto leaveJob = room->leaveRoom(); - connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] { - forgetJob->start(connectionData()); - // If the matching /sync response hasn't arrived yet, mark the room - // for explicit deletion - if (room->joinState() != JoinState::Leave) - d->roomIdsToForget.push_back(room->id()); - }); + connect(leaveJob, &BaseJob::result, this, + [this, leaveJob, forgetJob, room] { + if (leaveJob->error() == BaseJob::Success + || leaveJob->error() == BaseJob::NotFoundError) { + forgetJob->start(connectionData()); + // If the matching /sync response hasn't arrived yet, + // mark the room for explicit deletion + if (room->joinState() != JoinState::Leave) + d->roomIdsToForget.push_back(room->id()); + } else { + qCWarning(MAIN).nospace() + << "Error leaving room " << room->objectName() + << ": " << leaveJob->errorString(); + forgetJob->abandon(); + } + }); connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon); } else forgetJob->start(connectionData()); - connect(forgetJob, &BaseJob::success, this, [this, id] { - // Delete whatever instances of the room are still in the map. - for (auto f : { false, true }) - if (auto r = d->roomMap.take({ id, f })) { - qCDebug(MAIN) << "Room" << r->objectName() << "in state" - << toCString(r->joinState()) << "will be deleted"; - emit r->beforeDestruction(r); - r->deleteLater(); - } + connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] { + // Leave room in case of success, or room not known by server + if (forgetJob->error() == BaseJob::Success + || forgetJob->error() == BaseJob::NotFoundError) + d->removeRoom(id); // Delete the room from roomMap + else + qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": " + << forgetJob->errorString(); }); return forgetJob; } @@ -841,32 +854,34 @@ Room* Connection::room(const QString& roomId, JoinStates states) const Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const { - const auto id = d->roomAliasMap.value(roomAlias); + const auto id = d->roomAliasMap.value(serverPart(roomAlias)).value(roomAlias); if (!id.isEmpty()) return room(id, states); + qCWarning(MAIN) << "Room for alias" << roomAlias << "is not found under account" << userId(); return nullptr; } void Connection::updateRoomAliases(const QString& roomId, + const QString& aliasServer, const QStringList& previousRoomAliases, const QStringList& roomAliases) { + auto& aliasMap = d->roomAliasMap[aliasServer]; // Allocate if necessary for (const auto& a : previousRoomAliases) - if (d->roomAliasMap.remove(a) == 0) + if (aliasMap.remove(a) == 0) qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)"; for (const auto& a : roomAliases) { - auto& mappedId = d->roomAliasMap[a]; + auto& mappedId = aliasMap[a]; if (!mappedId.isEmpty()) { if (mappedId == roomId) qCDebug(MAIN) - << "Alias" << a << "is already mapped to room" << roomId; + << "Alias" << a << "is already mapped to" << roomId; else - qCWarning(MAIN) - << "Alias" << a << "will be force-remapped from room" - << mappedId << "to" << roomId; + qCWarning(MAIN) << "Alias" << a << "will be force-remapped from" + << mappedId << "to" << roomId; } mappedId = roomId; } @@ -904,8 +919,6 @@ QString Connection::userId() const { return d->userId; } QString Connection::deviceId() const { return d->data->deviceId(); } -QString Connection::token() const { return accessToken(); } - QByteArray Connection::accessToken() const { return d->data->accessToken(); } SyncJob* Connection::syncJob() const { return d->syncJob; } @@ -998,6 +1011,18 @@ Connection::DirectChatsMap Connection::directChats() const return d->directChats; } +// Removes room with given id from roomMap +void Connection::Private::removeRoom(const QString& roomId) +{ + for (auto f : { false, true }) + if (auto r = roomMap.take({ roomId, f })) { + qCDebug(MAIN) << "Room" << r->objectName() << "in state" + << toCString(r->joinState()) << "will be deleted"; + emit r->beforeDestruction(r); + r->deleteLater(); + } +} + void Connection::addToDirectChats(const Room* room, User* user) { Q_ASSERT(room != nullptr && user != nullptr); diff --git a/lib/connection.h b/lib/connection.h index a13def10..ef6cc156 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -132,7 +132,7 @@ public: explicit Connection(QObject* parent = nullptr); explicit Connection(const QUrl& server, QObject* parent = nullptr); - virtual ~Connection(); + ~Connection() override; /** Get all Invited and Joined rooms * \return a hashmap from a composite key - room name and whether @@ -261,9 +261,10 @@ public: JoinStates states = JoinState::Invite | JoinState::Join) const; /** Update the internal map of room aliases to IDs */ - /// This is used for internal bookkeeping of rooms. Do NOT use - /// it to try change aliases, use Room::setAliases instead - void updateRoomAliases(const QString& roomId, + /// This is used to maintain the internal index of room aliases. + /// It does NOT change aliases on the server, + /// \sa Room::setLocalAliases + void updateRoomAliases(const QString& roomId, const QString& aliasServer, const QStringList& previousRoomAliases, const QStringList& roomAliases); Q_INVOKABLE Room* invitation(const QString& roomId) const; @@ -276,7 +277,6 @@ public: Q_INVOKABLE SyncJob* syncJob() const; Q_INVOKABLE int millisToReconnect() const; - [[deprecated("Use accessToken() instead")]] Q_INVOKABLE QString token() const; Q_INVOKABLE void getTurnServers(); struct SupportedRoomVersion @@ -422,8 +422,8 @@ public slots: /** Set the homeserver base URL */ void setHomeserver(const QUrl& baseUrl); - /** Determine and set the homeserver from domain or MXID */ - void resolveServer(const QString& mxidOrDomain); + /** Determine and set the homeserver from MXID */ + void resolveServer(const QString& mxid); void connectToServer(const QString& user, const QString& password, const QString& initialDeviceName, diff --git a/lib/encryptionmanager.cpp b/lib/encryptionmanager.cpp new file mode 100644 index 00000000..61fcf9b4 --- /dev/null +++ b/lib/encryptionmanager.cpp @@ -0,0 +1,228 @@ +#include "encryptionmanager.h" + +#include "connection.h" + +#include "csapi/keys.h" + +#include <QtCore/QHash> +#include <QtCore/QStringBuilder> + +#include <account.h> // QtOlm +#include <functional> +#include <memory> + +using namespace QMatrixClient; +using namespace QtOlm; +using std::move; + +static const auto ed25519Name = QStringLiteral("ed25519"); +static const auto Curve25519Name = QStringLiteral("curve25519"); +static const auto SignedCurve25519Name = QStringLiteral("signed_curve25519"); +static const auto OlmV1Curve25519AesSha2AlgoName = + QStringLiteral("m.olm.v1.curve25519-aes-sha2"); +static const auto MegolmV1AesSha2AlgoName = + QStringLiteral("m.megolm.v1.aes-sha2"); +static const QStringList SupportedAlgorithms = { OlmV1Curve25519AesSha2AlgoName, + MegolmV1AesSha2AlgoName }; + +class EncryptionManager::Private +{ +public: + explicit Private(const QByteArray& encryptionAccountPickle, + float signedKeysProportion, float oneTimeKeyThreshold) + : signedKeysProportion(move(signedKeysProportion)) + , oneTimeKeyThreshold(move(oneTimeKeyThreshold)) + { + Q_ASSERT((0 <= signedKeysProportion) && (signedKeysProportion <= 1)); + Q_ASSERT((0 <= oneTimeKeyThreshold) && (oneTimeKeyThreshold <= 1)); + if (encryptionAccountPickle.isEmpty()) { + olmAccount.reset(new Account()); + } else { + olmAccount.reset( + new Account(encryptionAccountPickle)); // TODO: passphrase even + // with qtkeychain? + } + /* + * Note about targetKeysNumber: + * + * From: https://github.com/Zil0/matrix-python-sdk/ + * File: matrix_client/crypto/olm_device.py + * + * Try to maintain half the number of one-time keys libolm can hold + * uploaded on the HS. This is because some keys will be claimed by + * peers but not used instantly, and we want them to stay in libolm, + * until the limit is reached and it starts discarding keys, starting by + * the oldest. + */ + targetKeysNumber = olmAccount->maxOneTimeKeys(); // 2 // see note below + targetOneTimeKeyCounts = { + { SignedCurve25519Name, + qRound(signedKeysProportion * targetKeysNumber) }, + { Curve25519Name, + qRound((1 - signedKeysProportion) * targetKeysNumber) } + }; + } + ~Private() = default; + + UploadKeysJob* uploadIdentityKeysJob = nullptr; + UploadKeysJob* uploadOneTimeKeysJob = nullptr; + + QScopedPointer<Account> olmAccount; + + float signedKeysProportion; + float oneTimeKeyThreshold; + int targetKeysNumber; + + void updateKeysToUpload(); + bool oneTimeKeyShouldUpload(); + + QHash<QString, int> oneTimeKeyCounts; + void setOneTimeKeyCounts(const QHash<QString, int> oneTimeKeyCountsNewValue) + { + oneTimeKeyCounts = oneTimeKeyCountsNewValue; + updateKeysToUpload(); + } + QHash<QString, int> oneTimeKeysToUploadCounts; + QHash<QString, int> targetOneTimeKeyCounts; +}; + +EncryptionManager::EncryptionManager(const QByteArray& encryptionAccountPickle, + float signedKeysProportion, + float oneTimeKeyThreshold, QObject* parent) + : QObject(parent) + , d(std::make_unique<Private>(std::move(encryptionAccountPickle), + std::move(signedKeysProportion), + std::move(oneTimeKeyThreshold))) +{} + +EncryptionManager::~EncryptionManager() = default; + +void EncryptionManager::uploadIdentityKeys(Connection* connection) +{ + // https://matrix.org/docs/spec/client_server/latest#post-matrix-client-r0-keys-upload + DeviceKeys deviceKeys { + /* + * The ID of the user the device belongs to. Must match the user ID used + * when logging in. The ID of the device these keys belong to. Must + * match the device ID used when logging in. The encryption algorithms + * supported by this device. + */ + connection->userId(), + connection->deviceId(), + SupportedAlgorithms, + /* + * Public identity keys. The names of the properties should be in the + * format <algorithm>:<device_id>. The keys themselves should be encoded + * as specified by the key algorithm. + */ + { { Curve25519Name + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->curve25519IdentityKey() }, + { ed25519Name + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->ed25519IdentityKey() } }, + /* signatures should be provided after the unsigned deviceKeys + generation */ + {} + }; + + QJsonObject deviceKeysJsonObject = toJson(deviceKeys); + /* additionally removing signatures key, + * since we could not initialize deviceKeys + * without an empty signatures value: + */ + deviceKeysJsonObject.remove(QStringLiteral("signatures")); + /* + * Signatures for the device key object. + * A map from user ID, to a map from <algorithm>:<device_id> to the + * signature. The signature is calculated using the process called Signing + * JSON. + */ + deviceKeys.signatures = { + { connection->userId(), + { { ed25519Name + QStringLiteral(":") + connection->deviceId(), + d->olmAccount->sign(deviceKeysJsonObject) } } } + }; + + connect(d->uploadIdentityKeysJob, &BaseJob::success, this, [this] { + d->setOneTimeKeyCounts(d->uploadIdentityKeysJob->oneTimeKeyCounts()); + qDebug() << QString("Uploaded identity keys."); + }); + d->uploadIdentityKeysJob = connection->callApi<UploadKeysJob>(deviceKeys); +} + +void EncryptionManager::uploadOneTimeKeys(Connection* connection, + bool forceUpdate) +{ + if (forceUpdate || d->oneTimeKeyCounts.isEmpty()) { + auto job = connection->callApi<UploadKeysJob>(); + connect(job, &BaseJob::success, this, [job, this] { + d->setOneTimeKeyCounts(job->oneTimeKeyCounts()); + }); + } + + int signedKeysToUploadCount = + d->oneTimeKeysToUploadCounts.value(SignedCurve25519Name, 0); + int unsignedKeysToUploadCount = + d->oneTimeKeysToUploadCounts.value(Curve25519Name, 0); + + d->olmAccount->generateOneTimeKeys(signedKeysToUploadCount + + unsignedKeysToUploadCount); + + QHash<QString, QVariant> oneTimeKeys = {}; + const auto& olmAccountCurve25519OneTimeKeys = + d->olmAccount->curve25519OneTimeKeys(); + + int oneTimeKeysCounter = 0; + for (auto it = olmAccountCurve25519OneTimeKeys.cbegin(); + it != olmAccountCurve25519OneTimeKeys.cend(); ++it) { + QString keyId = it.key(); + QString keyType; + QVariant key; + if (oneTimeKeysCounter < signedKeysToUploadCount) { + QJsonObject message { { QStringLiteral("key"), + it.value().toString() } }; + key = d->olmAccount->sign(message); + keyType = SignedCurve25519Name; + + } else { + key = it.value(); + keyType = Curve25519Name; + } + ++oneTimeKeysCounter; + oneTimeKeys.insert(QString("%1:%2").arg(keyType).arg(keyId), key); + } + + d->uploadOneTimeKeysJob = connection->callApi<UploadKeysJob>(none, + oneTimeKeys); + d->olmAccount->markKeysAsPublished(); + qDebug() << QString("Uploaded new one-time keys: %1 signed, %2 unsigned.") + .arg(signedKeysToUploadCount) + .arg(unsignedKeysToUploadCount); +} + +QByteArray EncryptionManager::olmAccountPickle() +{ + return d->olmAccount->pickle(); // TODO: passphrase even with qtkeychain? +} + +void EncryptionManager::Private::updateKeysToUpload() +{ + for (auto it = targetOneTimeKeyCounts.cbegin(); + it != targetOneTimeKeyCounts.cend(); ++it) { + int numKeys = oneTimeKeyCounts.value(it.key(), 0); + int numToCreate = qMax(it.value() - numKeys, 0); + oneTimeKeysToUploadCounts.insert(it.key(), numToCreate); + } +} + +bool EncryptionManager::Private::oneTimeKeyShouldUpload() +{ + if (oneTimeKeyCounts.empty()) + return true; + for (auto it = targetOneTimeKeyCounts.cbegin(); + it != targetOneTimeKeyCounts.cend(); ++it) { + if (oneTimeKeyCounts.value(it.key(), 0) + < it.value() * oneTimeKeyThreshold) + return true; + } + return false; +} diff --git a/lib/encryptionmanager.h b/lib/encryptionmanager.h new file mode 100644 index 00000000..a41d88e1 --- /dev/null +++ b/lib/encryptionmanager.h @@ -0,0 +1,34 @@ +#pragma once + +#include <QtCore/QObject> + +#include <functional> +#include <memory> + +namespace QMatrixClient +{ +class Connection; + +class EncryptionManager : public QObject +{ + Q_OBJECT + +public: + // TODO: store constats separately? + // TODO: 0.5 oneTimeKeyThreshold instead of 0.1? + explicit EncryptionManager( + const QByteArray& encryptionAccountPickle = QByteArray(), + float signedKeysProportion = 1, float oneTimeKeyThreshold = float(0.1), + QObject* parent = nullptr); + ~EncryptionManager(); + + void uploadIdentityKeys(Connection* connection); + void uploadOneTimeKeys(Connection* connection, bool forceUpdate = false); + QByteArray olmAccountPickle(); + +private: + class Private; + std::unique_ptr<Private> d; +}; + +} // namespace QMatrixClient diff --git a/lib/events/encryptionevent.cpp b/lib/events/encryptionevent.cpp new file mode 100644 index 00000000..6aa7063b --- /dev/null +++ b/lib/events/encryptionevent.cpp @@ -0,0 +1,54 @@ +// +// Created by rusakov on 26/09/2017. +// Contributed by andreev on 27/06/2019. +// + +#include "encryptionevent.h" + +#include "converters.h" +#include "logging.h" + +#include <array> + +static const std::array<QString, 1> encryptionStrings = { { QStringLiteral( + "m.megolm.v1.aes-sha2") } }; + +namespace QMatrixClient +{ +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()); + + qCWarning(EVENTS) << "Unknown EncryptionType: " << encryptionString; + return EncryptionType::Undefined; + } +}; +} // namespace QMatrixClient + +using namespace QMatrixClient; + +EncryptionEventContent::EncryptionEventContent(const QJsonObject& json) + : encryption(fromJson<EncryptionType>(json["algorithm"_ls])) + , algorithm(sanitized(json["algorithm"_ls].toString())) + , rotationPeriodMs(json["rotation_period_ms"_ls].toInt(604800000)) + , rotationPeriodMsgs(json["rotation_period_msgs"_ls].toInt(100)) +{} + +void EncryptionEventContent::fillJson(QJsonObject* o) const +{ + Q_ASSERT(o); + Q_ASSERT_X( + encryption != EncryptionType::Undefined, __FUNCTION__, + "The key 'algorithm' must be explicit in EncryptionEventContent"); + if (encryption != EncryptionType::Undefined) + o->insert(QStringLiteral("algorithm"), algorithm); + o->insert(QStringLiteral("rotation_period_ms"), rotationPeriodMs); + o->insert(QStringLiteral("rotation_period_msgs"), rotationPeriodMsgs); +} diff --git a/lib/events/encryptionevent.h b/lib/events/encryptionevent.h new file mode 100644 index 00000000..97119c8d --- /dev/null +++ b/lib/events/encryptionevent.h @@ -0,0 +1,81 @@ +/****************************************************************************** + * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> + * + * This library is free software; you can redistribute it and/or + * modify it under the terms of the GNU Lesser General Public + * License as published by the Free Software Foundation; either + * version 2.1 of the License, or (at your option) any later version. + * + * This library is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + * Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public + * License along with this library; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + */ + +#pragma once + +#include "eventcontent.h" +#include "stateevent.h" + +namespace QMatrixClient +{ +class EncryptionEventContent : public EventContent::Base +{ +public: + enum EncryptionType : size_t + { + MegolmV1AesSha2 = 0, + Undefined + }; + + explicit EncryptionEventContent(EncryptionType et = Undefined) + : encryption(et) + {} + explicit EncryptionEventContent(const QJsonObject& json); + + EncryptionType encryption; + QString algorithm; + int rotationPeriodMs; + int rotationPeriodMsgs; + +protected: + void fillJson(QJsonObject* o) const override; +}; + +using EncryptionType = EncryptionEventContent::EncryptionType; + +class EncryptionEvent : public StateEvent<EncryptionEventContent> +{ + Q_GADGET +public: + DEFINE_EVENT_TYPEID("m.room.encryption", EncryptionEvent) + + using EncryptionType = EncryptionEventContent::EncryptionType; + + explicit EncryptionEvent(const QJsonObject& obj = {}) // TODO: apropriate + // default value + : StateEvent(typeId(), obj) + {} + template <typename... ArgTs> + EncryptionEvent(ArgTs&&... contentArgs) + : StateEvent(typeId(), matrixTypeId(), QString(), + std::forward<ArgTs>(contentArgs)...) + {} + + EncryptionType encryption() const { return content().encryption; } + + QString algorithm() const { return content().algorithm; } + int rotationPeriodMs() const { return content().rotationPeriodMs; } + int rotationPeriodMsgs() const { return content().rotationPeriodMsgs; } + +private: + REGISTER_ENUM(EncryptionType) +}; + +REGISTER_EVENT_TYPE(EncryptionEvent) +DEFINE_EVENTTYPE_ALIAS(Encryption, EncryptionEvent) +} // namespace QMatrixClient diff --git a/lib/events/event.h b/lib/events/event.h index 9dcec1ae..8056ccbe 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -61,14 +61,16 @@ static const auto TypeKey = QStringLiteral("type"); static const auto ContentKey = QStringLiteral("content"); static const auto EventIdKey = QStringLiteral("event_id"); static const auto UnsignedKey = QStringLiteral("unsigned"); +static const auto StateKeyKey = QStringLiteral("state_key"); static const auto TypeKeyL = "type"_ls; static const auto ContentKeyL = "content"_ls; static const auto EventIdKeyL = "event_id"_ls; static const auto UnsignedKeyL = "unsigned"_ls; static const auto RedactedCauseKeyL = "redacted_because"_ls; static const auto PrevContentKeyL = "prev_content"_ls; +static const auto StateKeyKeyL = "state_key"_ls; -// Minimal correct Matrix event JSON +/// Make a minimal correct Matrix event JSON template <typename StrT> inline QJsonObject basicEventJson(StrT matrixType, const QJsonObject& content) { @@ -257,7 +259,7 @@ public: } template <typename T> - T content(const QLatin1String& key) const + T content(QLatin1String key) const { return fromJson<T>(contentJson()[key]); } diff --git a/lib/events/eventcontent.h b/lib/events/eventcontent.h index d2b5e477..7a3db1fc 100644 --- a/lib/events/eventcontent.h +++ b/lib/events/eventcontent.h @@ -55,6 +55,9 @@ namespace EventContent QJsonObject originalJson; protected: + Base(const Base&) = default; + Base(Base&&) = default; + virtual void fillJson(QJsonObject* o) const = 0; }; @@ -172,13 +175,16 @@ namespace EventContent class TypedBase : public Base { public: - explicit TypedBase(const QJsonObject& o = {}) - : Base(o) + explicit TypedBase(QJsonObject o = {}) + : Base(std::move(o)) {} virtual QMimeType type() const = 0; virtual const FileInfo* fileInfo() const { return nullptr; } virtual FileInfo* fileInfo() { return nullptr; } virtual const Thumbnail* thumbnailInfo() const { return nullptr; } + + protected: + using Base::Base; }; /** diff --git a/lib/events/eventloader.h b/lib/events/eventloader.h index 9c797701..a203eaa3 100644 --- a/lib/events/eventloader.h +++ b/lib/events/eventloader.h @@ -34,7 +34,8 @@ namespace _impl } } // namespace _impl -/** Create an event with proper type from a JSON object +/*! Create an event with proper type from a JSON object + * * Use this factory template to detect the type from the JSON object * contents (the detected event type should derive from the template * parameter type) and create an event object of that type. @@ -45,7 +46,8 @@ inline event_ptr_tt<BaseEventT> loadEvent(const QJsonObject& fullJson) return _impl::loadEvent<BaseEventT>(fullJson, fullJson[TypeKeyL].toString()); } -/** Create an event from a type string and content JSON +/*! 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. @@ -58,6 +60,20 @@ inline event_ptr_tt<BaseEventT> loadEvent(const QString& matrixType, matrixType); } +/*! Create a state event from a type string, content JSON and state key + * + * Use this factory to resolve the C++ type from the Matrix type string + * in \p matrixType and create a state event of that type with content part + * set to \p content and state key set to \p stateKey (empty by default). + */ +inline StateEventPtr loadStateEvent(const QString& matrixType, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return _impl::loadEvent<StateEventBase>( + basicStateEventJson(matrixType, content, stateKey), matrixType); +} + template <typename EventT> struct JsonConverter<event_ptr_tt<EventT>> { diff --git a/lib/events/roomevent.cpp b/lib/events/roomevent.cpp index c28de559..513a99d0 100644 --- a/lib/events/roomevent.cpp +++ b/lib/events/roomevent.cpp @@ -74,7 +74,17 @@ QString RoomEvent::transactionId() const QString RoomEvent::stateKey() const { - return fullJson()["state_key"_ls].toString(); + return fullJson()[StateKeyKeyL].toString(); +} + +void RoomEvent::setRoomId(const QString& roomId) +{ + editJson().insert(QStringLiteral("room_id"), roomId); +} + +void RoomEvent::setSender(const QString& senderId) +{ + editJson().insert(QStringLiteral("sender"), senderId); } void RoomEvent::setTransactionId(const QString& txnId) diff --git a/lib/events/roomevent.h b/lib/events/roomevent.h index 42cd8fe4..dd0d25eb 100644 --- a/lib/events/roomevent.h +++ b/lib/events/roomevent.h @@ -60,6 +60,9 @@ public: QString transactionId() const; QString stateKey() const; + void setRoomId(const QString& roomId); + void setSender(const QString& senderId); + /** * Sets the transaction id for locally created events. This should be * done before the event is exposed to any code using the respective diff --git a/lib/events/roommemberevent.h b/lib/events/roommemberevent.h index a837b026..c1015df2 100644 --- a/lib/events/roommemberevent.h +++ b/lib/events/roommemberevent.h @@ -63,8 +63,14 @@ public: explicit RoomMemberEvent(const QJsonObject& obj) : StateEvent(typeId(), obj) {} - RoomMemberEvent(MemberEventContent&& c) - : StateEvent(typeId(), matrixTypeId(), c) + [[deprecated("Use RoomMemberEvent(userId, contentArgs) " + "instead")]] RoomMemberEvent(MemberEventContent&& c) + : StateEvent(typeId(), matrixTypeId(), QString(), c) + {} + template <typename... ArgTs> + RoomMemberEvent(const QString& userId, ArgTs&&... contentArgs) + : StateEvent(typeId(), matrixTypeId(), userId, + std::forward<ArgTs>(contentArgs)...) {} /// A special constructor to create unknown RoomMemberEvents @@ -82,7 +88,7 @@ public: {} MembershipType membership() const { return content().membership; } - QString userId() const { return fullJson()["state_key"_ls].toString(); } + QString userId() const { return fullJson()[StateKeyKeyL].toString(); } bool isDirect() const { return content().isDirect; } QString displayName() const { return content().displayName; } QUrl avatarUrl() const { return content().avatarUrl; } diff --git a/lib/events/simplestateevents.h b/lib/events/simplestateevents.h index 7ad2efa6..0078c44d 100644 --- a/lib/events/simplestateevents.h +++ b/lib/events/simplestateevents.h @@ -18,7 +18,6 @@ #pragma once -#include "converters.h" #include "stateevent.h" namespace QMatrixClient @@ -65,7 +64,7 @@ namespace EventContent {} \ template <typename T> \ explicit _Name(T&& value) \ - : StateEvent(typeId(), matrixTypeId(), \ + : StateEvent(typeId(), matrixTypeId(), QString(), \ QStringLiteral(#_ContentKey), std::forward<T>(value)) \ {} \ explicit _Name(QJsonObject obj) \ @@ -79,15 +78,27 @@ namespace EventContent DEFINE_SIMPLE_STATE_EVENT(RoomNameEvent, "m.room.name", QString, name) DEFINE_EVENTTYPE_ALIAS(RoomName, RoomNameEvent) -DEFINE_SIMPLE_STATE_EVENT(RoomAliasesEvent, "m.room.aliases", QStringList, - aliases) -DEFINE_EVENTTYPE_ALIAS(RoomAliases, RoomAliasesEvent) DEFINE_SIMPLE_STATE_EVENT(RoomCanonicalAliasEvent, "m.room.canonical_alias", QString, alias) DEFINE_EVENTTYPE_ALIAS(RoomCanonicalAlias, RoomCanonicalAliasEvent) DEFINE_SIMPLE_STATE_EVENT(RoomTopicEvent, "m.room.topic", QString, topic) DEFINE_EVENTTYPE_ALIAS(RoomTopic, RoomTopicEvent) -DEFINE_SIMPLE_STATE_EVENT(EncryptionEvent, "m.room.encryption", QString, - algorithm) DEFINE_EVENTTYPE_ALIAS(RoomEncryption, EncryptionEvent) + +class RoomAliasesEvent + : public StateEvent<EventContent::SimpleContent<QStringList>> +{ +public: + DEFINE_EVENT_TYPEID("m.room.aliases", RoomAliasesEvent) + explicit RoomAliasesEvent(const QJsonObject& obj) + : StateEvent(typeId(), obj, QStringLiteral("aliases")) + {} + RoomAliasesEvent(const QString& server, const QStringList& aliases) + : StateEvent(typeId(), matrixTypeId(), server, + QStringLiteral("aliases"), aliases) + {} + QString server() const { return stateKey(); } + QStringList aliases() const { return content().value; } +}; +REGISTER_EVENT_TYPE(RoomAliasesEvent) } // namespace QMatrixClient diff --git a/lib/events/stateevent.cpp b/lib/events/stateevent.cpp index 7fea59a1..bd228abd 100644 --- a/lib/events/stateevent.cpp +++ b/lib/events/stateevent.cpp @@ -26,7 +26,7 @@ using namespace QMatrixClient; [[gnu::unused]] static auto stateEventTypeInitialised = RoomEvent::factory_t::addMethod( [](const QJsonObject& json, const QString& matrixType) -> StateEventPtr { - if (!json.contains("state_key"_ls)) + if (!json.contains(StateKeyKeyL)) return nullptr; if (auto e = StateEventBase::factory_t::make(json, matrixType)) @@ -35,6 +35,12 @@ using namespace QMatrixClient; return makeEvent<StateEventBase>(unknownEventTypeId(), json); }); +StateEventBase::StateEventBase(Event::Type type, event_mtype_t matrixType, + const QString& stateKey, + const QJsonObject& contentJson) + : RoomEvent(type, basicStateEventJson(matrixType, contentJson, stateKey)) +{} + bool StateEventBase::repeatsState() const { const auto prevContentJson = unsignedJson().value(PrevContentKeyL); diff --git a/lib/events/stateevent.h b/lib/events/stateevent.h index 8a89c86c..d1b742ba 100644 --- a/lib/events/stateevent.h +++ b/lib/events/stateevent.h @@ -22,12 +22,29 @@ namespace QMatrixClient { + +/// Make a minimal correct Matrix state event JSON +template <typename StrT> +inline QJsonObject basicStateEventJson(StrT matrixType, + const QJsonObject& content, + const QString& stateKey = {}) +{ + return { { TypeKey, std::forward<StrT>(matrixType) }, + { StateKeyKey, stateKey }, + { ContentKey, content } }; +} + class StateEventBase : public RoomEvent { public: using factory_t = EventFactory<StateEventBase>; - using RoomEvent::RoomEvent; + StateEventBase(Type type, const QJsonObject& json) + : RoomEvent(type, json) + {} + StateEventBase(Type type, event_mtype_t matrixType, + const QString& stateKey = {}, + const QJsonObject& contentJson = {}); ~StateEventBase() override = default; bool isStateEvent() const override { return true; } @@ -87,8 +104,9 @@ public: } template <typename... ContentParamTs> explicit StateEvent(Type type, event_mtype_t matrixType, + const QString& stateKey, ContentParamTs&&... contentParams) - : StateEventBase(type, matrixType) + : StateEventBase(type, matrixType, stateKey) , _content(std::forward<ContentParamTs>(contentParams)...) { editJson().insert(ContentKey, _content.toJson()); diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 9c0b431c..f46d2d61 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -275,51 +275,23 @@ void BaseJob::gotReply() if (status().good()) setStatus(parseReply(d->reply.data())); else { - // FIXME: Factor out to smth like BaseJob::handleError() d->rawResponse = d->reply->readAll(); const auto jsonBody = d->reply->rawHeader("Content-Type") == "application/json"; qCDebug(d->logCat).noquote() << "Error body (truncated if long):" << d->rawResponse.left(500); - if (jsonBody) { - auto json = QJsonDocument::fromJson(d->rawResponse).object(); - const auto errCode = json.value("errcode"_ls).toString(); - if (error() == TooManyRequestsError - || errCode == "M_LIMIT_EXCEEDED") { - QString msg = tr("Too many requests"); - auto retryInterval = json.value("retry_after_ms"_ls).toInt(-1); - if (retryInterval != -1) - msg += - tr(", next retry advised after %1 ms").arg(retryInterval); - else // We still have to figure some reasonable interval - retryInterval = getNextRetryInterval(); - - setStatus(TooManyRequestsError, msg); - - // Shortcut to retry instead of executing finishJob() - stop(); - qCWarning(d->logCat) - << this << "will retry in" << retryInterval << "ms"; - d->retryTimer.start(retryInterval); - emit retryScheduled(d->retriesTaken, retryInterval); - return; - } - if (errCode == "M_CONSENT_NOT_GIVEN") { - d->status.code = UserConsentRequiredError; - d->errorUrl = json.value("consent_uri"_ls).toString(); - } else if (errCode == "M_UNSUPPORTED_ROOM_VERSION" - || errCode == "M_INCOMPATIBLE_ROOM_VERSION") { - d->status.code = UnsupportedRoomVersionError; - if (json.contains("room_version")) - d->status.message = - tr("Requested room version: %1") - .arg(json.value("room_version").toString()); - } else if (!json.isEmpty()) // Not localisable on the client side - setStatus(d->status.code, json.value("error"_ls).toString()); - } + if (jsonBody) + setStatus( + parseError(d->reply.data(), + QJsonDocument::fromJson(d->rawResponse).object())); } - finishJob(); + if (error() != TooManyRequestsError) + finishJob(); + else { + stop(); + emit retryScheduled(d->retriesTaken, d->retryTimer.interval()); + } } bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) @@ -348,34 +320,6 @@ bool checkContentType(const QByteArray& type, const QByteArrayList& patterns) return false; } -BaseJob::Status BaseJob::Status::fromHttpCode(int httpCode, QString msg) -{ - // clang-format off - return { [httpCode]() -> StatusCode { - if (httpCode / 10 == 41) // 41x errors - return httpCode == 410 ? IncorrectRequestError : NotFoundError; - switch (httpCode) { - case 401: case 403: case 407: - return ContentAccessError; - case 404: - return NotFoundError; - case 400: case 405: case 406: case 426: case 428: case 505: - case 494: // Unofficial nginx "Request header too large" - case 497: // Unofficial nginx "HTTP request sent to HTTPS port" - return IncorrectRequestError; - case 429: - return TooManyRequestsError; - case 501: case 510: - return RequestNotImplementedError; - case 511: - return NetworkAuthRequiredError; - default: - return NetworkError; - } - }(), msg }; - // clang-format on -} - BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const { // QNetworkReply error codes seem to be flawed when it comes to HTTP; @@ -409,7 +353,38 @@ BaseJob::Status BaseJob::doCheckReply(QNetworkReply* reply) const qCWarning(d->logCat).noquote().nospace() << this << urlString; qCWarning(d->logCat).noquote() << " " << httpCode << reason << replyState; - return Status::fromHttpCode(httpCode, reply->errorString()); + return { [httpCode]() -> StatusCode { + if (httpCode / 10 == 41) + return httpCode == 410 ? IncorrectRequestError + : NotFoundError; + switch (httpCode) { + case 401: + case 403: + case 407: + return ContentAccessError; + case 404: + return NotFoundError; + case 400: + case 405: + case 406: + case 426: + case 428: + case 505: + case 494: // Unofficial nginx "Request header too large" + case 497: // Unofficial nginx "HTTP request sent to HTTPS port" + return IncorrectRequestError; + case 429: + return TooManyRequestsError; + case 501: + case 510: + return RequestNotImplementedError; + case 511: + return NetworkAuthRequiredError; + default: + return NetworkError; + } + }(), + reply->errorString() }; } BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) @@ -425,8 +400,46 @@ BaseJob::Status BaseJob::parseReply(QNetworkReply* reply) BaseJob::Status BaseJob::parseJson(const QJsonDocument&) { return Success; } +BaseJob::Status BaseJob::parseError(QNetworkReply* reply, + const QJsonObject& errorJson) +{ + const auto errCode = errorJson.value("errcode"_ls).toString(); + if (error() == TooManyRequestsError || errCode == "M_LIMIT_EXCEEDED") { + QString msg = tr("Too many requests"); + auto retryInterval = errorJson.value("retry_after_ms"_ls).toInt(-1); + if (retryInterval != -1) + msg += tr(", next retry advised after %1 ms").arg(retryInterval); + else // We still have to figure some reasonable interval + retryInterval = getNextRetryInterval(); + + qCWarning(d->logCat) << this << "will retry in" << retryInterval << "ms"; + d->retryTimer.start(retryInterval); + + return { TooManyRequestsError, msg }; + } + if (errCode == "M_CONSENT_NOT_GIVEN") { + d->errorUrl = errorJson.value("consent_uri"_ls).toString(); + return { UserConsentRequiredError }; + } + if (errCode == "M_UNSUPPORTED_ROOM_VERSION" + || errCode == "M_INCOMPATIBLE_ROOM_VERSION") + return { UnsupportedRoomVersionError, + errorJson.contains("room_version"_ls) + ? tr("Requested room version: %1") + .arg(errorJson.value("room_version"_ls).toString()) + : errorJson.value("error"_ls).toString() }; + + // Not localisable on the client side + if (errorJson.contains("error"_ls)) + d->status.message = errorJson.value("error"_ls).toString(); + + return d->status; +} + void BaseJob::stop() { + // This method is used to semi-finalise the job before retrying; so + // stop the timeout timer but keep the retry timer running. d->timer.stop(); if (d->reply) { d->reply->disconnect(this); // Ignore whatever comes from the reply diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index 4d379f26..d94ab31d 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -116,8 +116,9 @@ public: * along the lines of StatusCode, with additional values * starting at UserDefinedError */ - struct Status + class Status { + public: Status(StatusCode c) : code(c) {} @@ -125,7 +126,6 @@ public: : code(c) , message(std::move(m)) {} - static Status fromHttpCode(int httpCode, QString msg = {}); bool good() const { return code < ErrorLevel; } friend QDebug operator<<(QDebug dbg, const Status& s) @@ -326,8 +326,7 @@ protected: * Processes the reply. By default, parses the reply into * a QJsonDocument and calls parseJson() if it's a valid JSON. * - * @param reply raw contents of a HTTP reply from the server (without - * headers) + * @param reply raw contents of a HTTP reply from the server * * @see gotReply, parseJson */ @@ -335,7 +334,7 @@ protected: /** * Processes the JSON document received from the Matrix server. - * By default returns succesful status without analysing the JSON. + * By default returns successful status without analysing the JSON. * * @param json valid JSON document received from the server * @@ -343,6 +342,15 @@ protected: */ virtual Status parseJson(const QJsonDocument&); + /** + * Processes the reply in case of unsuccessful HTTP code. + * The body is already loaded from the reply object to errorJson. + * @param reply the HTTP reply from the server + * @param errorJson the JSON payload describing the error + */ + virtual Status parseError(QNetworkReply* reply, + const QJsonObject& errorJson); + void setStatus(Status s); void setStatus(int code, QString message); diff --git a/lib/joinstate.h b/lib/joinstate.h index f7c0cb2b..e4dc679a 100644 --- a/lib/joinstate.h +++ b/lib/joinstate.h @@ -41,7 +41,7 @@ static const std::array<const char*, 3> JoinStateStrings { { "join", "invite", inline const char* toCString(JoinState js) { size_t state = size_t(js), index = 0; - while (state >>= 1) + while (state >>= 1u) ++index; return JoinStateStrings[index]; } diff --git a/lib/logging.cpp b/lib/logging.cpp index 73cc59a1..a7676c97 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -26,9 +26,9 @@ #endif // Use LOGGING_CATEGORY instead of Q_LOGGING_CATEGORY in the rest of the code -LOGGING_CATEGORY(MAIN, "libqmatrixclient.main") -LOGGING_CATEGORY(PROFILER, "libqmatrixclient.profiler") -LOGGING_CATEGORY(EVENTS, "libqmatrixclient.events") -LOGGING_CATEGORY(EPHEMERAL, "libqmatrixclient.events.ephemeral") -LOGGING_CATEGORY(JOBS, "libqmatrixclient.jobs") -LOGGING_CATEGORY(SYNCJOB, "libqmatrixclient.jobs.sync") +LOGGING_CATEGORY(MAIN, "quotient.main") +LOGGING_CATEGORY(PROFILER, "quotient.profiler") +LOGGING_CATEGORY(EVENTS, "quotient.events") +LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") +LOGGING_CATEGORY(JOBS, "quotient.jobs") +LOGGING_CATEGORY(SYNCJOB, "quotient.jobs.sync") diff --git a/lib/room.cpp b/lib/room.cpp index ec2a34ef..045cc7bc 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -41,6 +41,7 @@ #include "events/callcandidatesevent.h" #include "events/callhangupevent.h" #include "events/callinviteevent.h" +#include "events/encryptionevent.h" #include "events/receiptevent.h" #include "events/redactionevent.h" #include "events/roomavatarevent.h" @@ -101,12 +102,19 @@ public: /// The state of the room at timeline position before-0 /// \sa timelineBase std::unordered_map<StateEventKey, StateEventPtr> baseState; + /// State event stubs - events without content, just type and state key + static decltype(baseState) stubbedState; /// The state of the room at timeline position after-maxTimelineIndex() /// \sa Room::syncEdge QHash<StateEventKey, const StateEventBase*> currentState; + /// Servers with aliases for this room except the one of the local user + /// \sa Room::remoteAliases + QSet<QString> aliasServers; + Timeline timeline; PendingEvents unsyncedEvents; QHash<QString, TimelineItem::index_t> eventsIndex; + QString displayname; Avatar avatar; int highlightCount = 0; @@ -198,9 +206,22 @@ public: template <typename EventT> const EventT* getCurrentState(const QString& stateKey = {}) const { - static const EventT empty; - const auto* evt = - currentState.value({ EventT::matrixTypeId(), stateKey }, &empty); + const StateEventKey evtKey { EventT::matrixTypeId(), stateKey }; + const auto* evt = currentState.value(evtKey, nullptr); + if (!evt) { + if (stubbedState.find(evtKey) == stubbedState.end()) { + // In the absence of a real event, make a stub as-if an event + // with empty content has been received. Event classes should be + // prepared for empty/invalid/malicious content anyway. + stubbedState.emplace(evtKey, + loadStateEvent(EventT::matrixTypeId(), {}, + stateKey)); + qCDebug(MAIN) << "A new stub event created for key {" + << evtKey.first << evtKey.second << "}"; + } + evt = stubbedState[evtKey].get(); + Q_ASSERT(evt); + } Q_ASSERT(evt->type() == EventT::typeId() && evt->matrixType() == EventT::matrixTypeId()); return static_cast<const EventT*>(evt); @@ -275,24 +296,22 @@ public: QString doSendEvent(const RoomEvent* pEvent); void onEventSendingFailure(const QString& txnId, BaseJob* call = nullptr); - template <typename EvT> - SetRoomStateWithKeyJob* requestSetState(const QString& stateKey, - const EvT& event) + SetRoomStateWithKeyJob* requestSetState(const StateEventBase& event) { - if (q->successorId().isEmpty()) { - // TODO: Queue up state events sending (see #133). - return connection->callApi<SetRoomStateWithKeyJob>( - id, EvT::matrixTypeId(), stateKey, event.contentJson()); - } - qCWarning(MAIN) << q << "has been upgraded, state won't be set"; - return nullptr; + // if (event.roomId().isEmpty()) + // event.setRoomId(id); + // if (event.senderId().isEmpty()) + // event.setSender(connection->userId()); + // TODO: Queue up state events sending (see #133). + // TODO: Maybe addAsPending() as well, despite having no txnId + return connection->callApi<SetRoomStateWithKeyJob>( + id, event.matrixType(), event.stateKey(), event.contentJson()); } - template <typename EvT> - auto requestSetState(const EvT& event) + template <typename EvT, typename... ArgTs> + auto requestSetState(ArgTs&&... args) { - return connection->callApi<SetRoomStateJob>(id, EvT::matrixTypeId(), - event.contentJson()); + return requestSetState(EvT(std::forward<ArgTs>(args)...)); } /** @@ -316,6 +335,8 @@ private: bool isLocalUser(const User* u) const { return u == q->localUser(); } }; +decltype(Room::Private::baseState) Room::Private::stubbedState {}; + Room::Room(Connection* connection, QString id, JoinState initialJoinState) : QObject(connection) , d(new Private(connection, id, initialJoinState)) @@ -376,9 +397,20 @@ QString Room::name() const return d->getCurrentState<RoomNameEvent>()->name(); } -QStringList Room::aliases() const +QStringList Room::localAliases() const +{ + return d + ->getCurrentState<RoomAliasesEvent>( + connection()->homeserver().authority()) + ->aliases(); +} + +QStringList Room::remoteAliases() const { - return d->getCurrentState<RoomAliasesEvent>()->aliases(); + QStringList result; + for (const auto& s : d->aliasServers) + result += d->getCurrentState<RoomAliasesEvent>(s)->aliases(); + return result; } QString Room::canonicalAlias() const @@ -1279,6 +1311,10 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) { if (event->transactionId().isEmpty()) event->setTransactionId(connection->generateTxnId()); + if (event->roomId().isEmpty()) + event->setRoomId(id); + if (event->senderId().isEmpty()) + event->setSender(connection->userId()); auto* pEvent = rawPtr(event); emit q->pendingEventAboutToAdd(pEvent); unsyncedEvents.emplace_back(move(event)); @@ -1288,6 +1324,11 @@ RoomEvent* Room::Private::addAsPending(RoomEventPtr&& event) QString Room::Private::sendEvent(RoomEventPtr&& event) { + if (q->usesEncryption()) { + qCCritical(MAIN) << "Room" << q->objectName() + << "enforces encryption; sending encrypted messages " + "is not supported yet"; + } if (q->successorId().isEmpty()) return doSendEvent(addAsPending(std::move(event))); @@ -1484,39 +1525,39 @@ QString Room::postFile(const QString& plainText, const QUrl& localPath, QString Room::postEvent(RoomEvent* event) { - if (usesEncryption()) { - qCCritical(MAIN) << "Room" << displayName() - << "enforces encryption; sending encrypted messages " - "is not supported yet"; - } return d->sendEvent(RoomEventPtr(event)); } QString Room::postJson(const QString& matrixType, const QJsonObject& eventContent) { - return d->sendEvent( - loadEvent<RoomEvent>(basicEventJson(matrixType, eventContent))); + return d->sendEvent(loadEvent<RoomEvent>(matrixType, eventContent)); +} + +SetRoomStateWithKeyJob* Room::setState(const StateEventBase& evt) const +{ + return d->requestSetState(evt); } void Room::setName(const QString& newName) { - d->requestSetState(RoomNameEvent(newName)); + d->requestSetState<RoomNameEvent>(newName); } void Room::setCanonicalAlias(const QString& newAlias) { - d->requestSetState(RoomCanonicalAliasEvent(newAlias)); + d->requestSetState<RoomCanonicalAliasEvent>(newAlias); } -void Room::setAliases(const QStringList& aliases) +void Room::setLocalAliases(const QStringList& aliases) { - d->requestSetState(RoomAliasesEvent(aliases)); + d->requestSetState<RoomAliasesEvent>(connection()->homeserver().authority(), + aliases); } void Room::setTopic(const QString& newTopic) { - d->requestSetState(RoomTopicEvent(newTopic)); + d->requestSetState<RoomTopicEvent>(newTopic); } bool isEchoEvent(const RoomEventPtr& le, const PendingEventItem& re) @@ -1561,33 +1602,33 @@ void Room::inviteCall(const QString& callId, const int lifetime, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallInviteEvent(callId, lifetime, sdp)); + d->sendEvent<CallInviteEvent>(callId, lifetime, sdp); } void Room::sendCallCandidates(const QString& callId, const QJsonArray& candidates) { Q_ASSERT(supportsCalls()); - postEvent(new CallCandidatesEvent(callId, candidates)); + d->sendEvent<CallCandidatesEvent>(callId, candidates); } void Room::answerCall(const QString& callId, const int lifetime, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallAnswerEvent(callId, lifetime, sdp)); + d->sendEvent<CallAnswerEvent>(callId, lifetime, sdp); } void Room::answerCall(const QString& callId, const QString& sdp) { Q_ASSERT(supportsCalls()); - postEvent(new CallAnswerEvent(callId, sdp)); + d->sendEvent<CallAnswerEvent>(callId, sdp); } void Room::hangupCall(const QString& callId) { Q_ASSERT(supportsCalls()); - postEvent(new CallHangupEvent(callId)); + d->sendEvent<CallHangupEvent>(callId); } void Room::getPreviousContent(int limit) { d->getPreviousContent(limit); } @@ -1622,7 +1663,7 @@ LeaveRoomJob* Room::leaveRoom() SetRoomStateWithKeyJob* Room::setMemberState(const QString& memberId, const RoomMemberEvent& event) const { - return d->requestSetState(memberId, event); + return d->requestSetState<RoomMemberEvent>(memberId, event.content()); } void Room::kickMember(const QString& memberId, const QString& reason) @@ -1780,7 +1821,7 @@ RoomEventPtr makeRedacted(const RoomEvent& target, TypeKey, QStringLiteral("room_id"), QStringLiteral("sender"), - QStringLiteral("state_key"), + StateKeyKey, QStringLiteral("prev_content"), ContentKey, QStringLiteral("hashes"), @@ -2063,27 +2104,47 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (!is<RoomMemberEvent>(e)) // Room member events are too numerous qCDebug(EVENTS) << "Room state event:" << e; - return visit( - e, [](const RoomNameEvent&) { return NameChange; }, - [this, oldStateEvent](const RoomAliasesEvent& ae) { + // clang-format off + return visit(e + , [] (const RoomNameEvent&) { + return NameChange; + } + , [this,oldStateEvent] (const RoomAliasesEvent& ae) { + // clang-format on + if (ae.aliases().isEmpty()) { + qDebug(MAIN).noquote() + << ae.stateKey() << "no more has aliases for room" + << objectName(); + d->aliasServers.remove(ae.stateKey()); + } else { + d->aliasServers.insert(ae.stateKey()); + qDebug(MAIN).nospace().noquote() + << "New server with aliases for room " << objectName() + << ": " << ae.stateKey(); + } const auto previousAliases = oldStateEvent ? static_cast<const RoomAliasesEvent*>(oldStateEvent)->aliases() : QStringList(); - connection()->updateRoomAliases(id(), previousAliases, ae.aliases()); + connection()->updateRoomAliases(id(), ae.stateKey(), + previousAliases, ae.aliases()); return OtherChange; - }, - [this](const RoomCanonicalAliasEvent& evt) { + // clang-format off + } + , [this] (const RoomCanonicalAliasEvent& evt) { setObjectName(evt.alias().isEmpty() ? d->id : evt.alias()); return CanonicalAliasChange; - }, - [](const RoomTopicEvent&) { return TopicChange; }, - [this](const RoomAvatarEvent& evt) { + } + , [] (const RoomTopicEvent&) { + return TopicChange; + } + , [this] (const RoomAvatarEvent& evt) { if (d->avatar.updateUrl(evt.url())) emit avatarChanged(); return AvatarChange; - }, - [this, oldStateEvent](const RoomMemberEvent& evt) { + } + , [this,oldStateEvent] (const RoomMemberEvent& evt) { + // clang-format on auto* u = user(evt.userId()); const auto* oldMemberEvent = static_cast<const RoomMemberEvent*>(oldStateEvent); @@ -2150,27 +2211,30 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) d->membersLeft.append(u); } return MembersChange; - }, - [this](const EncryptionEvent&) { + // clang-format off + } + , [this] (const EncryptionEvent&) { emit encryption(); // It can only be done once, so emit it here. return OtherChange; - }, - [this](const RoomTombstoneEvent& evt) { + } + , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); if (auto* successor = connection()->room(successorId)) emit upgraded(evt.serverMessage(), successor); else connectUntil(connection(), &Connection::loadedRoomState, this, - [this, successorId, - serverMsg = evt.serverMessage()](Room* newRoom) { - if (newRoom->id() != successorId) - return false; - emit upgraded(serverMsg, newRoom); - return true; - }); + [this,successorId,serverMsg=evt.serverMessage()] + (Room* newRoom) { + if (newRoom->id() != successorId) + return false; + emit upgraded(serverMsg, newRoom); + return true; + }); return OtherChange; - }); + } + ); + // clang-format on } Room::Changes Room::processEphemeralEvent(EventPtr&& event) @@ -18,6 +18,7 @@ #pragma once +#include "connection.h" #include "eventitem.h" #include "joinstate.h" @@ -38,7 +39,6 @@ class Event; class Avatar; class SyncRoomData; class RoomMemberEvent; -class Connection; class User; class MemberSorter; class LeaveRoomJob; @@ -95,7 +95,8 @@ class Room : public QObject Q_PROPERTY(QString predecessorId READ predecessorId NOTIFY baseStateLoaded) Q_PROPERTY(QString successorId READ successorId NOTIFY upgraded) Q_PROPERTY(QString name READ name NOTIFY namesChanged) - Q_PROPERTY(QStringList aliases READ aliases NOTIFY namesChanged) + Q_PROPERTY(QStringList localAliases READ localAliases NOTIFY namesChanged) + Q_PROPERTY(QStringList remoteAliases READ remoteAliases NOTIFY namesChanged) Q_PROPERTY(QString canonicalAlias READ canonicalAlias NOTIFY namesChanged) Q_PROPERTY(QString displayName READ displayName NOTIFY displaynameChanged) Q_PROPERTY(QString topic READ topic NOTIFY topicChanged) @@ -176,7 +177,12 @@ public: QString predecessorId() const; QString successorId() const; QString name() const; - QStringList aliases() const; + /// Room aliases defined on the current user's server + /// \sa remoteAliases, setLocalAliases + QStringList localAliases() const; + /// Room aliases defined on other servers + /// \sa localAliases + QStringList remoteAliases() const; QString canonicalAlias() const; QString displayName() const; QString topic() const; @@ -420,16 +426,14 @@ public: MemberSorter memberSorter() const; - Q_INVOKABLE void inviteCall(const QString& callId, const int lifetime, - const QString& sdp); - Q_INVOKABLE void sendCallCandidates(const QString& callId, - const QJsonArray& candidates); - Q_INVOKABLE void answerCall(const QString& callId, const int lifetime, - const QString& sdp); - Q_INVOKABLE void answerCall(const QString& callId, const QString& sdp); - Q_INVOKABLE void hangupCall(const QString& callId); Q_INVOKABLE bool supportsCalls() const; + template <typename EvT, typename... ArgTs> + auto setState(ArgTs&&... args) const + { + return setState(EvT(std::forward<ArgTs>(args)...)); + } + public slots: /** Check whether the room should be upgraded */ void checkVersion(); @@ -451,9 +455,13 @@ public slots: QString postJson(const QString& matrixType, const QJsonObject& eventContent); QString retryMessage(const QString& txnId); void discardMessage(const QString& txnId); + + /// Send a request to update the room state with the given event + SetRoomStateWithKeyJob* setState(const StateEventBase& evt) const; void setName(const QString& newName); void setCanonicalAlias(const QString& newAlias); - void setAliases(const QStringList& aliases); + /// Set room aliases on the user's current server + void setLocalAliases(const QStringList& aliases); void setTopic(const QString& newTopic); /// You shouldn't normally call this method; it's here for debugging @@ -463,6 +471,7 @@ public slots: void inviteToRoom(const QString& memberId); LeaveRoomJob* leaveRoom(); + /// \deprecated - use setState() instead") SetRoomStateWithKeyJob* setMemberState(const QString& memberId, const RoomMemberEvent& event) const; void kickMember(const QString& memberId, const QString& reason = {}); @@ -485,6 +494,14 @@ public slots: /// Switch the room's version (aka upgrade) void switchVersion(QString newVersion); + void inviteCall(const QString& callId, const int lifetime, + const QString& sdp); + void sendCallCandidates(const QString& callId, const QJsonArray& candidates); + void answerCall(const QString& callId, const int lifetime, + const QString& sdp); + void answerCall(const QString& callId, const QString& sdp); + void hangupCall(const QString& callId); + signals: /// Initial set of state events has been loaded /** @@ -604,7 +621,6 @@ signals: void beforeDestruction(Room*); protected: - /// Returns true if any of room names/aliases has changed virtual Changes processStateEvent(const RoomEvent& e); virtual Changes processEphemeralEvent(EventPtr&& event); virtual Changes processAccountDataEvent(EventPtr&& event); diff --git a/lib/settings.cpp b/lib/settings.cpp index f8f1eae5..1278fe33 100644 --- a/lib/settings.cpp +++ b/lib/settings.cpp @@ -91,6 +91,8 @@ QMC_DEFINE_SETTING(AccountSettings, bool, keepLoggedIn, "keep_logged_in", false, static const auto HomeserverKey = QStringLiteral("homeserver"); static const auto AccessTokenKey = QStringLiteral("access_token"); +static const auto EncryptionAccountPickleKey = + QStringLiteral("encryption_account_pickle"); QUrl AccountSettings::homeserver() const { @@ -112,7 +114,8 @@ QString AccountSettings::accessToken() const void AccountSettings::setAccessToken(const QString& accessToken) { qCWarning(MAIN) << "Saving access_token to QSettings is insecure." - " Developers, please save access_token separately."; + " Developers, do it manually or contribute to share " + "QtKeychain logic to libQuotient."; setValue(AccessTokenKey, accessToken); } @@ -123,3 +126,25 @@ void AccountSettings::clearAccessToken() // re-issue it remove(AccessTokenKey); } + +QByteArray AccountSettings::encryptionAccountPickle() +{ + QString passphrase = ""; // FIXME: add QtKeychain + return value("encryption_account_pickle", "").toByteArray(); +} + +void AccountSettings::setEncryptionAccountPickle( + const QByteArray& encryptionAccountPickle) +{ + qCWarning(MAIN) + << "Saving encryption_account_pickle to QSettings is insecure." + " Developers, do it manually or contribute to share QtKeychain " + "logic to libQuotient."; + QString passphrase = ""; // FIXME: add QtKeychain + setValue("encryption_account_pickle", encryptionAccountPickle); +} + +void AccountSettings::clearEncryptionAccountPickle() +{ + remove(EncryptionAccountPickleKey); // TODO: Force to re-issue it? +} diff --git a/lib/settings.h b/lib/settings.h index cb09c479..e1ca0866 100644 --- a/lib/settings.h +++ b/lib/settings.h @@ -130,6 +130,8 @@ class AccountSettings : public SettingsGroup QMC_DECLARE_SETTING(bool, keepLoggedIn, setKeepLoggedIn) /** \deprecated \sa setAccessToken */ Q_PROPERTY(QString accessToken READ accessToken WRITE setAccessToken) + Q_PROPERTY(QByteArray encryptionAccountPickle READ encryptionAccountPickle + WRITE setEncryptionAccountPickle) public: template <typename... ArgTs> explicit AccountSettings(const QString& accountId, ArgTs... qsettingsArgs) @@ -147,5 +149,9 @@ public: * see QMatrixClient/Quaternion#181 */ void setAccessToken(const QString& accessToken); Q_INVOKABLE void clearAccessToken(); + + QByteArray encryptionAccountPickle(); + void setEncryptionAccountPickle(const QByteArray& encryptionAccountPickle); + Q_INVOKABLE void clearEncryptionAccountPickle(); }; } // namespace QMatrixClient diff --git a/lib/syncdata.h b/lib/syncdata.h index 49df8db6..6932878d 100644 --- a/lib/syncdata.h +++ b/lib/syncdata.h @@ -37,7 +37,7 @@ struct RoomSummary Omittable<int> joinedMemberCount; Omittable<int> invitedMemberCount; Omittable<QStringList> heroes; //< mxids of users to take part in the room - //name + // name bool isEmpty() const; /// Merge the contents of another RoomSummary object into this one diff --git a/lib/user.cpp b/lib/user.cpp index c463b42e..f0216454 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -269,8 +269,8 @@ void User::rename(const QString& newName, const Room* r) const auto actualNewName = sanitized(newName); MemberEventContent evtC; evtC.displayName = actualNewName; - connect(r->setMemberState(id(), RoomMemberEvent(move(evtC))), - &BaseJob::success, this, [=] { updateName(actualNewName, r); }); + connect(r->setState<RoomMemberEvent>(id(), move(evtC)), &BaseJob::success, + this, [=] { updateName(actualNewName, r); }); } bool User::setAvatar(const QString& fileName) diff --git a/lib/util.cpp b/lib/util.cpp index 4a7e0f61..9e0807c6 100644 --- a/lib/util.cpp +++ b/lib/util.cpp @@ -45,7 +45,7 @@ void QMatrixClient::linkifyUrls(QString& htmlEscapedText) // comma or dot static const QRegularExpression FullUrlRegExp( QStringLiteral( - R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet)://)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), + R"(\b((www\.(?!\.)(?!(\w|\.|-)+@)|(https?|ftp|magnet|matrix):(//)?)(&(?![lg]t;)|[^&\s<>'"])+(&(?![lg]t;)|[^&!,.\s<>'"\]):])))"), RegExpOptions); // email address: // [word chars, dots or dashes]@[word chars, dots or dashes].[word chars] @@ -113,6 +113,21 @@ qreal QMatrixClient::stringToHueF(const QString& string) return hueF; } +static const auto ServerPartRegEx = QStringLiteral( + "(\\[[^]]+\\]|[^:@]+)" // Either IPv6 address or hostname/IPv4 address + "(?::(\\d{1,5}))?" // Optional port +); + +QString QMatrixClient::serverPart(const QString& mxId) +{ + static QString re = "^[@!#$+].+?:(" // Localpart and colon + % ServerPartRegEx % ")$"; + static QRegularExpression parser( + re, + QRegularExpression::UseUnicodePropertiesOption); // Because Asian digits + return parser.match(mxId).captured(1); +} + // Tests for function_traits<> #ifdef Q_CC_CLANG @@ -327,26 +327,33 @@ inline std::pair<InputIt, ForwardIt> findFirstOf(InputIt first, InputIt last, void linkifyUrls(QString& htmlEscapedText); /** Sanitize the text before showing in HTML + * * This does toHtmlEscaped() and removes Unicode BiDi marks. */ QString sanitized(const QString& plainText); /** Pretty-print plain text into HTML + * * This includes HTML escaping of <,>,",& and calling linkifyUrls() */ QString prettyPrint(const QString& plainText); /** Return a path to cache directory after making sure that it exists + * * The returned path has a trailing slash, clients don't need to append it. * \param dir path to cache directory relative to the standard cache path */ QString cacheLocation(const QString& dirName); /** Hue color component of based of the hash of the string. + * * The implementation is based on XEP-0392: * https://xmpp.org/extensions/xep-0392.html * Naming and range are the same as QColor's hueF method: * https://doc.qt.io/qt-5/qcolor.html#integer-vs-floating-point-precision */ qreal stringToHueF(const QString& string); + +/** Extract the serverpart from MXID */ +QString serverPart(const QString& mxId); } // namespace QMatrixClient |