aboutsummaryrefslogtreecommitdiff
path: root/lib/connection.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'lib/connection.cpp')
-rw-r--r--lib/connection.cpp2596
1 files changed, 1932 insertions, 664 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp
index 6bdedf26..4547474a 100644
--- a/lib/connection.cpp
+++ b/lib/connection.cpp
@@ -1,62 +1,76 @@
-/******************************************************************************
- * Copyright (C) 2015 Felix Rohrbach <kde@fxrh.de>
- *
- * This library is free software; you can redistribute it and/or
- * modify it under the terms of the GNU Lesser General Public
- * License as published by the Free Software Foundation; either
- * version 2.1 of the License, or (at your option) any later version.
- *
- * This library is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
- * Lesser General Public License for more details.
- *
- * You should have received a copy of the GNU Lesser General Public
- * License along with this library; if not, write to the Free Software
- * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
- */
+// SPDX-FileCopyrightText: 2016 Kitsune Ral <Kitsune-Ral@users.sf.net>
+// SPDX-FileCopyrightText: 2017 Roman Plášil <me@rplasil.name>
+// SPDX-FileCopyrightText: 2019 Ville Ranki <ville.ranki@iki.fi>
+// SPDX-FileCopyrightText: 2019 Alexey Andreyev <aa13q@ya.ru>
+// SPDX-License-Identifier: LGPL-2.1-or-later
#include "connection.h"
+
+#include "accountregistry.h"
#include "connectiondata.h"
-#include "user.h"
-#include "events/directchatevent.h"
-#include "events/eventloader.h"
+#include "qt_connection_util.h"
#include "room.h"
#include "settings.h"
-#include "csapi/login.h"
-#include "csapi/logout.h"
-#include "csapi/receipts.h"
-#include "csapi/leaving.h"
+#include "user.h"
+
+// NB: since Qt 6, moc_connection.cpp needs Room and User fully defined
+#include "moc_connection.cpp"
+
#include "csapi/account-data.h"
+#include "csapi/capabilities.h"
#include "csapi/joining.h"
-#include "csapi/to_device.h"
+#include "csapi/leaving.h"
+#include "csapi/logout.h"
#include "csapi/room_send.h"
-#include "jobs/syncjob.h"
-#include "jobs/mediathumbnailjob.h"
-#include "jobs/downloadfilejob.h"
+#include "csapi/to_device.h"
#include "csapi/voip.h"
+#include "csapi/wellknown.h"
+#include "csapi/whoami.h"
-#include <QtNetwork/QDnsLookup>
-#include <QtCore/QFile>
+#include "events/directchatevent.h"
+#include "jobs/downloadfilejob.h"
+#include "jobs/mediathumbnailjob.h"
+#include "jobs/syncjob.h"
+#include <variant>
+
+#ifdef Quotient_E2EE_ENABLED
+# include "database.h"
+# include "keyverificationsession.h"
+
+# include "e2ee/qolmaccount.h"
+# include "e2ee/qolminboundsession.h"
+# include "e2ee/qolmsession.h"
+# include "e2ee/qolmutility.h"
+# include "e2ee/qolmutils.h"
+
+# include "events/keyverificationevent.h"
+#endif // Quotient_E2EE_ENABLED
+
+#if QT_VERSION_MAJOR >= 6
+# include <qt6keychain/keychain.h>
+#else
+# include <qt5keychain/keychain.h>
+#endif
+
+#include <QtCore/QCoreApplication>
#include <QtCore/QDir>
-#include <QtCore/QFileInfo>
-#include <QtCore/QStandardPaths>
-#include <QtCore/QStringBuilder>
#include <QtCore/QElapsedTimer>
+#include <QtCore/QFile>
+#include <QtCore/QMimeDatabase>
#include <QtCore/QRegularExpression>
-#include <QtCore/QCoreApplication>
+#include <QtCore/QStandardPaths>
+#include <QtCore/QStringBuilder>
+#include <QtNetwork/QDnsLookup>
-using namespace QMatrixClient;
+using namespace Quotient;
// This is very much Qt-specific; STL iterators don't have key() and value()
template <typename HashT, typename Pred>
-HashT erase_if(HashT& hashMap, Pred pred)
+HashT remove_if(HashT& hashMap, Pred pred)
{
HashT removals;
- for (auto it = hashMap.begin(); it != hashMap.end();)
- {
- if (pred(it))
- {
+ for (auto it = hashMap.begin(); it != hashMap.end();) {
+ if (pred(it)) {
removals.insert(it.key(), it.value());
it = hashMap.erase(it);
} else
@@ -65,479 +79,1180 @@ HashT erase_if(HashT& hashMap, Pred pred)
return removals;
}
-#ifndef TRIM_RAW_DATA
-#define TRIM_RAW_DATA 65535
+class Connection::Private {
+public:
+ explicit Private(std::unique_ptr<ConnectionData>&& connection)
+ : data(move(connection))
+ {}
+
+ Connection* q = nullptr;
+ std::unique_ptr<ConnectionData> data;
+ // A complex key below is a pair of room name and whether its
+ // state is Invited. The spec mandates to keep Invited room state
+ // separately; specifically, we should keep objects for Invite and
+ // Leave state of the same room if the two happen to co-exist.
+ QHash<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<QString> pendingStateRoomIds;
+ QMap<QString, User*> userMap;
+ DirectChatsMap directChats;
+ DirectChatUsersMap directChatUsers;
+ // The below two variables track local changes between sync completions.
+ // See https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events
+ DirectChatsMap dcLocalAdditions;
+ DirectChatsMap dcLocalRemovals;
+ UnorderedMap<QString, EventPtr> accountData;
+ QMetaObject::Connection syncLoopConnection {};
+ int syncTimeout = -1;
+
+#ifdef Quotient_E2EE_ENABLED
+ QSet<QString> trackedUsers;
+ QSet<QString> outdatedUsers;
+ QHash<QString, QHash<QString, DeviceKeys>> deviceKeys;
+ QueryKeysJob *currentQueryKeysJob = nullptr;
+ bool encryptionUpdateRequired = false;
+ PicklingMode picklingMode = Unencrypted {};
+ Database *database = nullptr;
+ QHash<QString, int> oneTimeKeysCount;
+ std::vector<std::unique_ptr<EncryptedEvent>> pendingEncryptedEvents;
+ void handleEncryptedToDeviceEvent(const EncryptedEvent& event);
+ bool processIfVerificationEvent(const Event &evt, bool encrypted);
+
+ // A map from SenderKey to vector of InboundSession
+ UnorderedMap<QString, std::vector<QOlmSessionPtr>> olmSessions;
+
+ QHash<QString, KeyVerificationSession*> verificationSessions;
+#endif
+
+ GetCapabilitiesJob* capabilitiesJob = nullptr;
+ GetCapabilitiesJob::Capabilities capabilities;
+
+ QVector<GetLoginFlowsJob::LoginFlow> loginFlows;
+
+#ifdef Quotient_E2EE_ENABLED
+ std::unique_ptr<QOlmAccount> olmAccount;
+ bool isUploadingKeys = false;
+ bool firstSync = true;
+#endif // Quotient_E2EE_ENABLED
+
+ QPointer<GetWellknownJob> resolverJob = nullptr;
+ QPointer<GetLoginFlowsJob> loginFlowsJob = nullptr;
+
+ SyncJob* syncJob = nullptr;
+ QPointer<LogoutJob> logoutJob = nullptr;
+
+ bool cacheState = true;
+ bool cacheToBinary =
+ SettingsGroup("libQuotient").get("cache_type",
+ SettingsGroup("libQMatrixClient").get<QString>("cache_type"))
+ != "json";
+ bool lazyLoading = false;
+
+ /** \brief Check the homeserver and resolve it if needed, before connecting
+ *
+ * A single entry for functions that need to check whether the homeserver
+ * is valid before running. May execute connectFn either synchronously
+ * or asynchronously. In case of errors, emits resolveError() if
+ * the homeserver URL is not valid and cannot be resolved from userId, or
+ * the homeserver doesn't support the requested login flow.
+ *
+ * \param userId fully-qualified MXID to resolve HS from
+ * \param connectFn a function to execute once the HS URL is good
+ * \param flow optionally, a login flow that should be supported for
+ * connectFn to work; `none`, if there's no login flow
+ * requirements
+ * \sa resolveServer, resolveError
+ */
+ void checkAndConnect(const QString &userId,
+ const std::function<void ()> &connectFn,
+ const std::optional<LoginFlow> &flow = none);
+ template <typename... LoginArgTs>
+ void loginToServer(LoginArgTs&&... loginArgs);
+ void completeSetup(const QString &mxId);
+ void removeRoom(const QString& roomId);
+
+ void consumeRoomData(SyncDataList&& roomDataList, bool fromCache);
+ void consumeAccountData(Events&& accountDataEvents);
+ void consumePresenceData(Events&& presenceData);
+ void consumeToDeviceEvents(Events&& toDeviceEvents);
+ void consumeDevicesList(DevicesList&& devicesList);
+
+ void packAndSendAccountData(EventPtr&& event)
+ {
+ const auto eventType = event->matrixType();
+ q->callApi<SetAccountDataJob>(data->userId(), eventType,
+ event->contentJson());
+ accountData[eventType] = std::move(event);
+ emit q->accountDataChanged(eventType);
+ }
+
+ template <EventClass EventT, typename ContentT>
+ void packAndSendAccountData(ContentT&& content)
+ {
+ packAndSendAccountData(
+ makeEvent<EventT>(std::forward<ContentT>(content)));
+ }
+ QString topLevelStatePath() const
+ {
+ return q->stateCacheDir().filePath("state.json");
+ }
+
+#ifdef Quotient_E2EE_ENABLED
+ void loadSessions() {
+ olmSessions = q->database()->loadOlmSessions(picklingMode);
+ }
+ void saveSession(const QOlmSession& session, const QString& senderKey) const
+ {
+ q->database()->saveOlmSession(senderKey, session.sessionId(),
+ session.pickle(picklingMode),
+ QDateTime::currentDateTime());
+ }
+
+ template <typename FnT>
+ std::pair<QString, QString> doDecryptMessage(const QOlmSession& session,
+ const QOlmMessage& message,
+ FnT&& andThen) const
+ {
+ const auto expectedMessage = session.decrypt(message);
+ if (expectedMessage) {
+ const auto result =
+ std::make_pair(*expectedMessage, session.sessionId());
+ andThen();
+ return result;
+ }
+ const auto errorLine = message.type() == QOlmMessage::PreKey
+ ? "Failed to decrypt prekey message:"
+ : "Failed to decrypt message:";
+ qCDebug(E2EE) << errorLine << expectedMessage.error();
+ return {};
+ }
+
+ std::pair<QString, QString> sessionDecryptMessage(
+ const QJsonObject& personalCipherObject, const QByteArray& senderKey)
+ {
+ const auto msgType = static_cast<QOlmMessage::Type>(
+ personalCipherObject.value(TypeKeyL).toInt(-1));
+ if (msgType != QOlmMessage::General && msgType != QOlmMessage::PreKey) {
+ qCWarning(E2EE) << "Olm message has incorrect type" << msgType;
+ return {};
+ }
+ QOlmMessage message {
+ personalCipherObject.value(BodyKeyL).toString().toLatin1(), msgType
+ };
+ for (const auto& session : olmSessions[senderKey])
+ if (msgType == QOlmMessage::General
+ || session->matchesInboundSessionFrom(senderKey, message)) {
+ return doDecryptMessage(*session, message, [this, &session] {
+ q->database()->setOlmSessionLastReceived(
+ session->sessionId(), QDateTime::currentDateTime());
+ });
+ }
+
+ if (msgType == QOlmMessage::General) {
+ qCWarning(E2EE) << "Failed to decrypt message";
+ return {};
+ }
+
+ qCDebug(E2EE) << "Creating new inbound session"; // Pre-key messages only
+ auto newSessionResult =
+ olmAccount->createInboundSessionFrom(senderKey, message);
+ if (!newSessionResult) {
+ qCWarning(E2EE)
+ << "Failed to create inbound session for" << senderKey
+ << "with error" << newSessionResult.error();
+ return {};
+ }
+ auto newSession = std::move(*newSessionResult);
+ if (olmAccount->removeOneTimeKeys(*newSession) != OLM_SUCCESS) {
+ qWarning(E2EE) << "Failed to remove one time key for session"
+ << newSession->sessionId();
+ // Keep going though
+ }
+ return doDecryptMessage(
+ *newSession, message, [this, &senderKey, &newSession] {
+ saveSession(*newSession, senderKey);
+ olmSessions[senderKey].push_back(std::move(newSession));
+ });
+ }
#endif
-class Connection::Private
-{
- public:
- explicit Private(std::unique_ptr<ConnectionData>&& connection)
- : data(move(connection))
- { }
- Q_DISABLE_COPY(Private)
- Private(Private&&) = delete;
- Private operator=(Private&&) = delete;
-
- Connection* q = nullptr;
- std::unique_ptr<ConnectionData> data;
- // A complex key below is a pair of room name and whether its
- // state is Invited. The spec mandates to keep Invited room state
- // separately so we should, e.g., keep objects for Invite and
- // Leave state of the same room.
- QHash<QPair<QString, bool>, Room*> roomMap;
- QVector<QString> roomIdsToForget;
- QVector<Room*> firstTimeRooms;
- QMap<QString, User*> userMap;
- DirectChatsMap directChats;
- DirectChatUsersMap directChatUsers;
- std::unordered_map<QString, EventPtr> accountData;
- QString userId;
-
- SyncJob* syncJob = nullptr;
-
- bool cacheState = true;
- bool cacheToBinary = SettingsGroup("libqmatrixclient")
- .value("cache_type").toString() != "json";
-
- void connectWithToken(const QString& user, const QString& accessToken,
- const QString& deviceId);
- void broadcastDirectChatUpdates(const DirectChatsMap& additions,
- const DirectChatsMap& removals);
-
- template <typename EventT>
- EventT* unpackAccountData() const
- {
- const auto& eventIt = accountData.find(EventT::matrixTypeId());
- return eventIt == accountData.end()
- ? nullptr : weakPtrCast<EventT>(eventIt->second);
+ std::pair<EventPtr, QString> sessionDecryptMessage(const EncryptedEvent& encryptedEvent)
+ {
+#ifndef Quotient_E2EE_ENABLED
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+ return {};
+#else
+ if (encryptedEvent.algorithm() != OlmV1Curve25519AesSha2AlgoKey)
+ return {};
+
+ const auto identityKey = olmAccount->identityKeys().curve25519;
+ const auto personalCipherObject =
+ encryptedEvent.ciphertext(identityKey);
+ if (personalCipherObject.isEmpty()) {
+ qCDebug(E2EE) << "Encrypted event is not for the current device";
+ return {};
+ }
+ const auto [decrypted, olmSessionId] =
+ sessionDecryptMessage(personalCipherObject,
+ encryptedEvent.senderKey().toLatin1());
+ if (decrypted.isEmpty()) {
+ qCDebug(E2EE) << "Problem with new session from senderKey:"
+ << encryptedEvent.senderKey()
+ << olmAccount->oneTimeKeys().keys;
+ return {};
}
- void packAndSendAccountData(EventPtr&& event)
- {
- const auto eventType = event->matrixType();
- q->callApi<SetAccountDataJob>(userId, eventType,
- event->contentJson());
- accountData[eventType] = std::move(event);
- emit q->accountDataChanged(eventType);
+ auto&& decryptedEvent =
+ fromJson<EventPtr>(QJsonDocument::fromJson(decrypted.toUtf8()));
+
+ if (auto sender = decryptedEvent->fullJson()[SenderKeyL].toString();
+ sender != encryptedEvent.senderId()) {
+ qCWarning(E2EE) << "Found user" << sender
+ << "instead of sender" << encryptedEvent.senderId()
+ << "in Olm plaintext";
+ return {};
}
- template <typename EventT, typename ContentT>
- void packAndSendAccountData(ContentT&& content)
- {
- packAndSendAccountData(
- makeEvent<EventT>(std::forward<ContentT>(content)));
+ auto query = database->prepareQuery(QStringLiteral("SELECT edKey FROM tracked_devices WHERE curveKey=:curveKey;"));
+ query.bindValue(":curveKey", encryptedEvent.contentJson()["sender_key"].toString());
+ database->execute(query);
+ if (!query.next()) {
+ qCWarning(E2EE) << "Received olm message from unknown device" << encryptedEvent.contentJson()["sender_key"].toString();
+ return {};
}
+ auto edKey = decryptedEvent->fullJson()["keys"]["ed25519"].toString();
+ if (edKey.isEmpty() || query.value(QStringLiteral("edKey")).toString() != edKey) {
+ qCDebug(E2EE) << "Received olm message with invalid ed key";
+ return {};
+ }
+
+ // TODO: keys to constants
+ const auto decryptedEventObject = decryptedEvent->fullJson();
+ const auto recipient = decryptedEventObject.value("recipient"_ls).toString();
+ if (recipient != data->userId()) {
+ qCDebug(E2EE) << "Found user" << recipient << "instead of us"
+ << data->userId() << "in Olm plaintext";
+ return {};
+ }
+ const auto ourKey = decryptedEventObject.value("recipient_keys"_ls).toObject()
+ .value(Ed25519Key).toString();
+ if (ourKey != QString::fromUtf8(olmAccount->identityKeys().ed25519)) {
+ qCDebug(E2EE) << "Found key" << ourKey
+ << "instead of ours own ed25519 key"
+ << olmAccount->identityKeys().ed25519
+ << "in Olm plaintext";
+ return {};
+ }
+
+ return { std::move(decryptedEvent), olmSessionId };
+#endif // Quotient_E2EE_ENABLED
+ }
+#ifdef Quotient_E2EE_ENABLED
+ bool isKnownCurveKey(const QString& userId, const QString& curveKey) const;
+
+ void loadOutdatedUserDevices();
+ void saveDevicesList();
+ void loadDevicesList();
+
+ // This function assumes that an olm session with (user, device) exists
+ std::pair<QOlmMessage::Type, QByteArray> olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const;
+ bool createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const OneTimeKeys &oneTimeKeyObject);
+ QString curveKeyForUserDevice(const QString& userId,
+ const QString& device) const;
+ QJsonObject assembleEncryptedContent(QJsonObject payloadJson,
+ const QString& targetUserId,
+ const QString& targetDeviceId) const;
+#endif
+
+ void saveAccessTokenToKeychain() const
+ {
+ qCDebug(MAIN) << "Saving access token to keychain for" << q->userId();
+ auto job = new QKeychain::WritePasswordJob(qAppName());
+ job->setAutoDelete(true);
+ job->setKey(q->userId());
+ job->setBinaryData(data->accessToken());
+ job->start();
+ //TODO error handling
+ }
+
+ void dropAccessToken()
+ {
+ qCDebug(MAIN) << "Removing access token from keychain for" << q->userId();
+ auto job = new QKeychain::DeletePasswordJob(qAppName());
+ job->setAutoDelete(true);
+ job->setKey(q->userId());
+ job->start();
+
+ auto pickleJob = new QKeychain::DeletePasswordJob(qAppName());
+ pickleJob->setAutoDelete(true);
+ pickleJob->setKey(q->userId() + "-Pickle"_ls);
+ pickleJob->start();
+ //TODO error handling
+
+ data->setToken({});
+ }
};
Connection::Connection(const QUrl& server, QObject* parent)
: QObject(parent)
- , d(std::make_unique<Private>(std::make_unique<ConnectionData>(server)))
+ , d(makeImpl<Private>(std::make_unique<ConnectionData>(server)))
{
+#ifdef Quotient_E2EE_ENABLED
+ //connect(qApp, &QCoreApplication::aboutToQuit, this, &Connection::saveOlmAccount);
+#endif
d->q = this; // All d initialization should occur before this line
}
-Connection::Connection(QObject* parent)
- : Connection({}, parent)
-{ }
+Connection::Connection(QObject* parent) : Connection({}, parent) {}
Connection::~Connection()
{
- qCDebug(MAIN) << "deconstructing connection object for" << d->userId;
+ qCDebug(MAIN) << "deconstructing connection object for" << userId();
stopSync();
+ Accounts.drop(this);
}
-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.
+ if (isJobPending(d->resolverJob))
+ d->resolverJob->abandon();
- // 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())
- {
- emit resolveError(
- tr("%1 is not a valid homeserver address")
- .arg(maybeBaseUrl.toString()));
+ if (maybeBaseUrl.isEmpty() || !maybeBaseUrl.isValid()) {
+ emit resolveError(tr("%1 is not a valid homeserver address")
+ .arg(maybeBaseUrl.toString()));
return;
}
- setHomeserver(maybeBaseUrl);
- emit resolved();
- return;
-
- // FIXME, #178: The below code is incorrect and is no more executed. The
- // correct server resolution should be done from .well-known/matrix/client
- auto domain = maybeBaseUrl.host();
- qCDebug(MAIN) << "Finding the server" << domain;
- // Check if the Matrix server has a dedicated service record.
- auto* dns = new QDnsLookup();
- dns->setType(QDnsLookup::SRV);
- dns->setName("_matrix._tcp." + domain);
-
- connect(dns, &QDnsLookup::finished, [this,dns,maybeBaseUrl]() {
- QUrl baseUrl { maybeBaseUrl };
- if (dns->error() == QDnsLookup::NoError &&
- dns->serviceRecords().isEmpty())
- {
- auto record = dns->serviceRecords().front();
- baseUrl.setHost(record.target());
- baseUrl.setPort(record.port());
- qCDebug(MAIN) << "SRV record for" << maybeBaseUrl.host()
- << "is" << baseUrl.authority();
+ qCDebug(MAIN) << "Finding the server" << maybeBaseUrl.host();
+
+ const auto& oldBaseUrl = d->data->baseUrl();
+ d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call
+ d->resolverJob = callApi<GetWellknownJob>();
+ // Connect to finished() to make sure baseUrl is restored in any case
+ connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl, oldBaseUrl] {
+ // Revert baseUrl so that setHomeserver() below triggers signals
+ // in case the base URL actually changed
+ d->data->setBaseUrl(oldBaseUrl);
+ if (d->resolverJob->error() == BaseJob::Abandoned)
+ return;
+
+ if (d->resolverJob->error() != BaseJob::NotFound) {
+ if (!d->resolverJob->status().good()) {
+ qCWarning(MAIN)
+ << "Fetching .well-known file failed, FAIL_PROMPT";
+ emit resolveError(tr("Failed resolving the homeserver"));
+ return;
+ }
+ QUrl baseUrl { d->resolverJob->data().homeserver.baseUrl };
+ if (baseUrl.isEmpty()) {
+ qCWarning(MAIN) << "base_url not provided, FAIL_PROMPT";
+ emit resolveError(
+ tr("The homeserver base URL is not provided"));
+ return;
+ }
+ if (!baseUrl.isValid()) {
+ qCWarning(MAIN) << "base_url invalid, FAIL_ERROR";
+ emit resolveError(tr("The homeserver base URL is invalid"));
+ return;
+ }
+ qCInfo(MAIN) << ".well-known URL for" << maybeBaseUrl.host() << "is"
+ << baseUrl.toString();
+ setHomeserver(baseUrl);
} else {
- qCDebug(MAIN) << baseUrl.host() << "doesn't have SRV record"
- << dns->name() << "- using the hostname as is";
+ qCInfo(MAIN) << "No .well-known file, using" << maybeBaseUrl
+ << "for base URL";
+ setHomeserver(maybeBaseUrl);
}
- setHomeserver(baseUrl);
- emit resolved();
- dns->deleteLater();
+ Q_ASSERT(d->loginFlowsJob != nullptr); // Ensured by setHomeserver()
});
- dns->lookup();
}
-void Connection::connectToServer(const QString& user, const QString& password,
- const QString& initialDeviceName,
- const QString& deviceId)
+inline UserIdentifier makeUserIdentifier(const QString& id)
{
- checkAndConnect(user,
- [=] {
- doConnectToServer(user, password, initialDeviceName, deviceId);
- });
+ return { QStringLiteral("m.id.user"), { { QStringLiteral("user"), id } } };
}
-void Connection::doConnectToServer(const QString& user, const QString& password,
+
+inline UserIdentifier make3rdPartyIdentifier(const QString& medium,
+ const QString& address)
+{
+ return { QStringLiteral("m.id.thirdparty"),
+ { { QStringLiteral("medium"), medium },
+ { QStringLiteral("address"), address } } };
+}
+
+void Connection::loginWithPassword(const QString& userId,
+ const QString& password,
const QString& initialDeviceName,
const QString& deviceId)
{
- auto loginJob = callApi<LoginJob>(QStringLiteral("m.login.password"),
- UserIdentifier { QStringLiteral("m.id.user"),
- {{ QStringLiteral("user"), user }} },
- password, /*token*/ "", deviceId, initialDeviceName);
- connect(loginJob, &BaseJob::success, this,
- [this, loginJob] {
- d->connectWithToken(loginJob->userId(), loginJob->accessToken(),
- loginJob->deviceId());
+ d->checkAndConnect(userId, [=,this] {
+ d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId),
+ password, /*token*/ "", deviceId, initialDeviceName);
+ }, LoginFlows::Password);
+}
+
+SsoSession* Connection::prepareForSso(const QString& initialDeviceName,
+ const QString& deviceId)
+{
+ return new SsoSession(this, initialDeviceName, deviceId);
+}
+
+void Connection::loginWithToken(const QByteArray& loginToken,
+ const QString& initialDeviceName,
+ const QString& deviceId)
+{
+ Q_ASSERT(d->data->baseUrl().isValid() && d->loginFlows.contains(LoginFlows::Token));
+ d->loginToServer(LoginFlows::Token.type,
+ none /*user is encoded in loginToken*/, "" /*password*/,
+ loginToken, deviceId, initialDeviceName);
+}
+
+void Connection::assumeIdentity(const QString& mxId, const QString& accessToken,
+ const QString& deviceId)
+{
+ d->checkAndConnect(mxId, [this, mxId, accessToken, deviceId] {
+ d->data->setToken(accessToken.toLatin1());
+ d->data->setDeviceId(deviceId); // Can't we deduce this from access_token?
+ auto* job = callApi<GetTokenOwnerJob>();
+ connect(job, &BaseJob::success, this, [this, job, mxId] {
+ if (mxId != job->userId())
+ qCWarning(MAIN).nospace()
+ << "The access_token owner (" << job->userId()
+ << ") is different from passed MXID (" << mxId << ")!";
+ d->completeSetup(job->userId());
});
- connect(loginJob, &BaseJob::failure, this,
- [this, loginJob] {
- emit loginError(loginJob->errorString(),
- loginJob->rawData(TRIM_RAW_DATA));
+ connect(job, &BaseJob::failure, this, [this, job] {
+ emit loginError(job->errorString(), job->rawDataSample());
});
+ });
}
-void Connection::connectWithToken(const QString& userId,
- const QString& accessToken,
- const QString& deviceId)
+void Connection::reloadCapabilities()
{
- checkAndConnect(userId,
- [=] { d->connectWithToken(userId, accessToken, deviceId); });
+ d->capabilitiesJob = callApi<GetCapabilitiesJob>(BackgroundRequest);
+ connect(d->capabilitiesJob, &BaseJob::success, this, [this] {
+ d->capabilities = d->capabilitiesJob->capabilities();
+
+ if (d->capabilities.roomVersions) {
+ qCDebug(MAIN) << "Room versions:" << defaultRoomVersion()
+ << "is default, full list:" << availableRoomVersions();
+ emit capabilitiesLoaded();
+ for (auto* r: std::as_const(d->roomMap))
+ r->checkVersion();
+ } else
+ qCWarning(MAIN)
+ << "The server returned an empty set of supported versions;"
+ " disabling version upgrade recommendations to reduce noise";
+ });
+ connect(d->capabilitiesJob, &BaseJob::failure, this, [this] {
+ if (d->capabilitiesJob->error() == BaseJob::IncorrectRequest)
+ qCDebug(MAIN) << "Server doesn't support /capabilities;"
+ " version upgrade recommendations won't be issued";
+ });
+}
+
+bool Connection::loadingCapabilities() const
+{
+ // (Ab)use the fact that room versions cannot be omitted after
+ // the capabilities have been loaded (see reloadCapabilities() above).
+ return !d->capabilities.roomVersions;
}
-void Connection::Private::connectWithToken(const QString& user,
- const QString& accessToken,
- const QString& deviceId)
+template <typename... LoginArgTs>
+void Connection::Private::loginToServer(LoginArgTs&&... loginArgs)
{
- userId = user;
+ auto loginJob =
+ q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...);
+ connect(loginJob, &BaseJob::success, q, [this, loginJob] {
+ data->setToken(loginJob->accessToken().toLatin1());
+ data->setDeviceId(loginJob->deviceId());
+ completeSetup(loginJob->userId());
+ saveAccessTokenToKeychain();
+#ifdef Quotient_E2EE_ENABLED
+ database->clear();
+#endif
+ });
+ connect(loginJob, &BaseJob::failure, q, [this, loginJob] {
+ emit q->loginError(loginJob->errorString(), loginJob->rawDataSample());
+ });
+}
+
+void Connection::Private::completeSetup(const QString& mxId)
+{
+ data->setUserId(mxId);
q->user(); // Creates a User object for the local user
- data->setToken(accessToken.toLatin1());
- data->setDeviceId(deviceId);
+ q->setObjectName(data->userId() % '/' % data->deviceId());
qCDebug(MAIN) << "Using server" << data->baseUrl().toDisplayString()
- << "by user" << userId << "from device" << deviceId;
+ << "by user" << data->userId()
+ << "from device" << data->deviceId();
+ Accounts.add(q);
+ connect(qApp, &QCoreApplication::aboutToQuit, q, &Connection::saveState);
+#ifndef Quotient_E2EE_ENABLED
+ qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off.";
+#else // Quotient_E2EE_ENABLED
+ AccountSettings accountSettings(data->userId());
+
+ QKeychain::ReadPasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ QEventLoop loop;
+ QKeychain::ReadPasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error() == QKeychain::Error::EntryNotFound) {
+ picklingMode = Encrypted { RandomBuffer(128) };
+ QKeychain::WritePasswordJob job(qAppName());
+ job.setAutoDelete(false);
+ job.setKey(accountSettings.userId() + QStringLiteral("-Pickle"));
+ job.setBinaryData(std::get<Encrypted>(picklingMode).key);
+ QEventLoop loop;
+ QKeychain::WritePasswordJob::connect(&job, &QKeychain::Job::finished, &loop, &QEventLoop::quit);
+ job.start();
+ loop.exec();
+
+ if (job.error()) {
+ qCWarning(E2EE) << "Could not save pickling key to keychain: " << job.errorString();
+ }
+ } else if(job.error() != QKeychain::Error::NoError) {
+ //TODO Error, do something
+ qCWarning(E2EE) << "Error loading pickling key from keychain:" << job.error();
+ } else {
+ qCDebug(E2EE) << "Successfully loaded pickling key from keychain";
+ picklingMode = Encrypted { job.binaryData() };
+ }
+
+ database = new Database(data->userId(), data->deviceId(), q);
+
+ // init olmAccount
+ olmAccount = std::make_unique<QOlmAccount>(data->userId(), data->deviceId(), q);
+ connect(olmAccount.get(), &QOlmAccount::needsSave, q, &Connection::saveOlmAccount);
+
+ loadSessions();
+
+ if (database->accountPickle().isEmpty()) {
+ // create new account and save unpickle data
+ olmAccount->createNewAccount();
+ auto job = q->callApi<UploadKeysJob>(olmAccount->deviceKeys());
+ connect(job, &BaseJob::failure, q, [job]{
+ qCWarning(E2EE) << "Failed to upload device keys:" << job->errorString();
+ });
+ } else {
+ // account already existing
+ if (!olmAccount->unpickle(database->accountPickle(), picklingMode))
+ qWarning(E2EE)
+ << "Could not unpickle Olm account, E2EE won't be available";
+ }
+#endif // Quotient_E2EE_ENABLED
emit q->stateChanged();
emit q->connected();
-
+ q->reloadCapabilities();
}
-void Connection::checkAndConnect(const QString& userId,
- std::function<void()> connectFn)
+void Connection::Private::checkAndConnect(const QString& userId,
+ const std::function<void()>& connectFn,
+ const std::optional<LoginFlow>& flow)
{
- if (d->data->baseUrl().isValid())
- {
+ if (data->baseUrl().isValid() && (!flow || loginFlows.contains(*flow))) {
connectFn();
return;
}
- // Not good to go, try to fix the homeserver URL.
- if (userId.startsWith('@') && userId.indexOf(':') != -1)
- {
- connectSingleShot(this, &Connection::homeserverChanged, this, connectFn);
- // NB: doResolveServer can emit resolveError, so this is a part of
- // checkAndConnect function contract.
- resolveServer(userId);
+ // Not good to go, try to ascertain the homeserver URL and flows
+ if (userId.startsWith('@') && userId.indexOf(':') != -1) {
+ q->resolveServer(userId);
+ if (flow)
+ connectSingleShot(q, &Connection::loginFlowsChanged, q,
+ [this, flow, connectFn] {
+ if (loginFlows.contains(*flow))
+ connectFn();
+ else
+ emit q->loginError(
+ tr("Unsupported login flow"),
+ tr("The homeserver at %1 does not support"
+ " the login flow '%2'")
+ .arg(data->baseUrl().toDisplayString(),
+ flow->type));
+ });
+ else
+ connectSingleShot(q, &Connection::homeserverChanged, q, connectFn);
} else
- emit resolveError(
- tr("%1 is an invalid homeserver URL")
- .arg(d->data->baseUrl().toString()));
+ emit q->resolveError(tr("Please provide the fully-qualified user id"
+ " (such as @user:example.org) so that the"
+ " homeserver could be resolved; the current"
+ " homeserver URL(%1) is not good")
+ .arg(data->baseUrl().toDisplayString()));
}
void Connection::logout()
{
- auto job = callApi<LogoutJob>();
- connect( job, &LogoutJob::success, this, [this] {
- stopSync();
- d->data->setToken({});
- emit stateChanged();
- emit loggedOut();
+ // If there's an ongoing sync job, stop it (this also suspends sync loop)
+ const auto wasSyncing = bool(d->syncJob);
+ if (wasSyncing)
+ {
+ d->syncJob->abandon();
+ d->syncJob = nullptr;
+ }
+
+ d->logoutJob = callApi<LogoutJob>();
+ emit stateChanged(); // isLoggedIn() == false from now
+
+ connect(d->logoutJob, &LogoutJob::finished, this, [this, wasSyncing] {
+ if (d->logoutJob->status().good()
+ || d->logoutJob->error() == BaseJob::Unauthorised
+ || d->logoutJob->error() == BaseJob::ContentAccessError) {
+ if (d->syncLoopConnection)
+ disconnect(d->syncLoopConnection);
+ SettingsGroup("Accounts").remove(userId());
+ d->dropAccessToken();
+ emit loggedOut();
+ deleteLater();
+ } else { // logout() somehow didn't proceed - restore the session state
+ emit stateChanged();
+ if (wasSyncing)
+ syncLoopIteration(); // Resume sync loop (or a single sync)
+ }
});
}
void Connection::sync(int timeout)
{
- if (d->syncJob)
+ if (d->syncJob) {
+ qCInfo(MAIN) << d->syncJob << "is already running";
return;
+ }
+ if (!isLoggedIn()) {
+ qCWarning(MAIN) << "Not logged in, not going to sync";
+ return;
+ }
- // Raw string: http://en.cppreference.com/w/cpp/language/string_literal
- const auto filter =
- QStringLiteral(R"({"room": { "timeline": { "limit": 100 } } })");
- auto job = d->syncJob = callApi<SyncJob>(BackgroundRequest,
- d->data->lastEvent(), filter, timeout);
- connect( job, &SyncJob::success, this, [this, job] {
+ d->syncTimeout = timeout;
+ Filter filter;
+ filter.room.timeline.limit.emplace(100);
+ filter.room.state.lazyLoadMembers.emplace(d->lazyLoading);
+ auto job = d->syncJob =
+ callApi<SyncJob>(BackgroundRequest, d->data->lastEvent(), filter,
+ timeout);
+ connect(job, &SyncJob::success, this, [this, job] {
onSyncSuccess(job->takeData());
d->syncJob = nullptr;
emit syncDone();
});
- connect( job, &SyncJob::retryScheduled, this,
- [this,job] (int retriesTaken, int nextInMilliseconds)
- {
- emit networkError(job->errorString(), job->rawData(TRIM_RAW_DATA),
- retriesTaken, nextInMilliseconds);
- });
- connect( job, &SyncJob::failure, this, [this, job] {
- d->syncJob = nullptr;
- if (job->error() == BaseJob::ContentAccessError)
- {
+ connect(job, &SyncJob::retryScheduled, this,
+ [this, job](int retriesTaken, int nextInMilliseconds) {
+ emit networkError(job->errorString(), job->rawDataSample(),
+ retriesTaken, nextInMilliseconds);
+ });
+ connect(job, &SyncJob::failure, this, [this, job] {
+ // SyncJob persists with retries on transient errors; if it fails,
+ // there's likely something serious enough to stop the loop.
+ stopSync();
+ if (job->error() == BaseJob::Unauthorised) {
qCWarning(SYNCJOB)
- << "Sync job failed with ContentAccessError - login expired?";
- emit loginError(job->errorString(), job->rawData(TRIM_RAW_DATA));
- }
- else
- emit syncError(job->errorString(), job->rawData(TRIM_RAW_DATA));
+ << "Sync job failed with Unauthorised - login expired?";
+ emit loginError(job->errorString(), job->rawDataSample());
+ } else
+ emit syncError(job->errorString(), job->rawDataSample());
});
}
-void Connection::onSyncSuccess(SyncData &&data) {
+void Connection::syncLoop(int timeout)
+{
+ if (d->syncLoopConnection && d->syncTimeout == timeout) {
+ qCInfo(MAIN) << "Attempt to run sync loop but there's one already "
+ "running; nothing will be done";
+ return;
+ }
+ std::swap(d->syncTimeout, timeout);
+ if (d->syncLoopConnection) {
+ qCInfo(MAIN) << "Timeout for next syncs changed from"
+ << timeout << "to" << d->syncTimeout;
+ } else {
+ d->syncLoopConnection = connect(this, &Connection::syncDone,
+ this, &Connection::syncLoopIteration,
+ Qt::QueuedConnection);
+ syncLoopIteration(); // initial sync to start the loop
+ }
+}
+
+void Connection::syncLoopIteration()
+{
+ if (isLoggedIn())
+ sync(d->syncTimeout);
+ else
+ qCInfo(MAIN) << "Logged out, sync loop will stop now";
+}
+
+QJsonObject toJson(const DirectChatsMap& directChats)
+{
+ QJsonObject json;
+ for (auto it = directChats.begin(); it != directChats.end();) {
+ QJsonArray roomIds;
+ const auto* user = it.key();
+ for (; it != directChats.end() && it.key() == user; ++it)
+ roomIds.append(*it);
+ json.insert(user->id(), roomIds);
+ }
+ return json;
+}
+
+void Connection::onSyncSuccess(SyncData&& data, bool fromCache)
+{
+#ifdef Quotient_E2EE_ENABLED
+ d->oneTimeKeysCount = data.deviceOneTimeKeysCount();
+ if (d->oneTimeKeysCount[SignedCurve25519Key] < 0.4 * d->olmAccount->maxNumberOfOneTimeKeys()
+ && !d->isUploadingKeys) {
+ d->isUploadingKeys = true;
+ d->olmAccount->generateOneTimeKeys(
+ d->olmAccount->maxNumberOfOneTimeKeys() / 2 - d->oneTimeKeysCount[SignedCurve25519Key]);
+ auto keys = d->olmAccount->oneTimeKeys();
+ auto job = d->olmAccount->createUploadKeyRequest(keys);
+ run(job, ForegroundRequest);
+ connect(job, &BaseJob::success, this,
+ [this] { d->olmAccount->markKeysAsPublished(); });
+ connect(job, &BaseJob::result, this,
+ [this] { d->isUploadingKeys = false; });
+ }
+ if(d->firstSync) {
+ d->loadDevicesList();
+ d->firstSync = false;
+ }
+
+ d->consumeDevicesList(data.takeDevicesList());
+#endif // Quotient_E2EE_ENABLED
+ d->consumeToDeviceEvents(data.takeToDeviceEvents());
d->data->setLastEvent(data.nextBatch());
- for (auto&& roomData: data.takeRoomData())
- {
- const auto forgetIdx = d->roomIdsToForget.indexOf(roomData.roomId);
- if (forgetIdx != -1)
- {
- d->roomIdsToForget.removeAt(forgetIdx);
- if (roomData.joinState == JoinState::Leave)
- {
- qDebug(MAIN) << "Room" << roomData.roomId
+ d->consumeRoomData(data.takeRoomData(), fromCache);
+ d->consumeAccountData(data.takeAccountData());
+ d->consumePresenceData(data.takePresenceData());
+#ifdef Quotient_E2EE_ENABLED
+ if(d->encryptionUpdateRequired) {
+ d->loadOutdatedUserDevices();
+ d->encryptionUpdateRequired = false;
+ }
+#endif
+}
+
+void Connection::Private::consumeRoomData(SyncDataList&& roomDataList,
+ bool fromCache)
+{
+ for (auto&& roomData: roomDataList) {
+ const auto forgetIdx = roomIdsToForget.indexOf(roomData.roomId);
+ if (forgetIdx != -1) {
+ roomIdsToForget.removeAt(forgetIdx);
+ if (roomData.joinState == JoinState::Leave) {
+ qDebug(MAIN)
+ << "Room" << roomData.roomId
<< "has been forgotten, ignoring /sync response for it";
continue;
}
qWarning(MAIN) << "Room" << roomData.roomId
- << "has just been forgotten but /sync returned it in"
- << toCString(roomData.joinState)
- << "state - suspiciously fast turnaround";
+ << "has just been forgotten but /sync returned it in"
+ << terse << roomData.joinState
+ << "state - suspiciously fast turnaround";
}
- if ( auto* r = provideRoom(roomData.roomId, roomData.joinState) )
- {
- r->updateData(std::move(roomData));
- if (d->firstTimeRooms.removeOne(r))
- emit loadedRoomState(r);
+ if (auto* r = q->provideRoom(roomData.roomId, roomData.joinState)) {
+ pendingStateRoomIds.removeOne(roomData.roomId);
+ // 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);
}
- QCoreApplication::processEvents();
}
- for (auto&& accountEvent: data.takeAccountData())
- {
- if (is<DirectChatEvent>(*accountEvent))
- {
- const auto usersToDCs = ptrCast<DirectChatEvent>(move(accountEvent))
- ->usersToDirectChats();
- DirectChatsMap removals =
- erase_if(d->directChats, [&usersToDCs] (auto it) {
- return !usersToDCs.contains(it.key()->id(), it.value());
+}
+
+void Connection::Private::consumeAccountData(Events&& accountDataEvents)
+{
+ // After running this loop, the account data events not saved in
+ // accountData (see the end of the loop body) are auto-cleaned away
+ for (auto&& eventPtr: accountDataEvents) {
+ switchOnType(*eventPtr,
+ [this](const DirectChatEvent& dce) {
+ // https://github.com/quotient-im/libQuotient/wiki/Handling-direct-chat-events
+ const auto& usersToDCs = dce.usersToDirectChats();
+ DirectChatsMap remoteRemovals =
+ remove_if(directChats, [&usersToDCs, this](auto it) {
+ return !(
+ usersToDCs.contains(it.key()->id(), it.value())
+ || dcLocalAdditions.contains(it.key(), it.value()));
+ });
+ remove_if(directChatUsers, [&remoteRemovals](auto it) {
+ return remoteRemovals.contains(it.value(), it.key());
});
- erase_if(d->directChatUsers, [&usersToDCs] (auto it) {
- return !usersToDCs.contains(it.value()->id(), it.key());
- });
- if (MAIN().isDebugEnabled())
- for (auto it = removals.begin(); it != removals.end(); ++it)
- qCDebug(MAIN) << it.value()
- << "is no more a direct chat with" << it.key()->id();
-
- DirectChatsMap additions;
- for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it)
- {
- if (auto* u = user(it.key()))
- {
- if (!d->directChats.contains(u, it.value()))
- {
- Q_ASSERT(!d->directChatUsers.contains(it.value(), u));
- additions.insert(u, it.value());
- d->directChats.insert(u, it.value());
- d->directChatUsers.insert(it.value(), u);
- qCDebug(MAIN) << "Marked room" << it.value()
- << "as a direct chat with" << u->id();
+ // Remove from dcLocalRemovals what the server already has.
+ remove_if(dcLocalRemovals, [&remoteRemovals](auto it) {
+ return remoteRemovals.contains(it.key(), it.value());
+ });
+ if (MAIN().isDebugEnabled())
+ for (auto it = remoteRemovals.begin();
+ it != remoteRemovals.end(); ++it) {
+ qCDebug(MAIN)
+ << it.value() << "is no more a direct chat with"
+ << it.key()->id();
}
- } else
- qCWarning(MAIN)
+
+ DirectChatsMap remoteAdditions;
+ for (auto it = usersToDCs.begin(); it != usersToDCs.end(); ++it) {
+ if (auto* u = q->user(it.key())) {
+ if (!directChats.contains(u, it.value())
+ && !dcLocalRemovals.contains(u, it.value())) {
+ Q_ASSERT(!directChatUsers.contains(it.value(), u));
+ remoteAdditions.insert(u, it.value());
+ directChats.insert(u, it.value());
+ directChatUsers.insert(it.value(), u);
+ qCDebug(MAIN) << "Marked room" << it.value()
+ << "as a direct chat with" << u->id();
+ }
+ } else
+ qCWarning(MAIN)
<< "Couldn't get a user object for" << it.key();
- }
- if (!additions.isEmpty() || !removals.isEmpty())
- emit directChatsListChanged(additions, removals);
+ }
+ // Remove from dcLocalAdditions what the server already has.
+ remove_if(dcLocalAdditions, [&remoteAdditions](auto it) {
+ return remoteAdditions.contains(it.key(), it.value());
+ });
+ if (!remoteAdditions.isEmpty() || !remoteRemovals.isEmpty())
+ emit q->directChatsListChanged(remoteAdditions,
+ remoteRemovals);
+ },
+ // catch-all, passing eventPtr for a possible take-over
+ [this, &eventPtr](const Event& accountEvent) {
+ if (is<IgnoredUsersEvent>(accountEvent))
+ qCDebug(MAIN)
+ << "Users ignored by" << data->userId() << "updated:"
+ << QStringList(q->ignoredUsers().values()).join(',');
+
+ auto& currentData = accountData[accountEvent.matrixType()];
+ // A polymorphic event-specific comparison might be a bit
+ // more efficient; maaybe do it another day
+ if (!currentData
+ || currentData->contentJson() != accountEvent.contentJson()) {
+ currentData = std::move(eventPtr);
+ qCDebug(MAIN) << "Updated account data of type"
+ << currentData->matrixType();
+ emit q->accountDataChanged(currentData->matrixType());
+ }
+ });
+ }
+ if (!dcLocalAdditions.isEmpty() || !dcLocalRemovals.isEmpty()) {
+ qDebug(MAIN) << "Sending updated direct chats to the server:"
+ << dcLocalRemovals.size() << "removal(s),"
+ << dcLocalAdditions.size() << "addition(s)";
+ q->callApi<SetAccountDataJob>(data->userId(), QStringLiteral("m.direct"),
+ toJson(directChats));
+ dcLocalAdditions.clear();
+ dcLocalRemovals.clear();
+ }
+}
- continue;
+void Connection::Private::consumePresenceData(Events&& presenceData)
+{
+ // To be implemented
+}
+
+void Connection::Private::consumeToDeviceEvents(Events&& toDeviceEvents)
+{
+#ifdef Quotient_E2EE_ENABLED
+ if (!toDeviceEvents.empty()) {
+ qCDebug(E2EE) << "Consuming" << toDeviceEvents.size()
+ << "to-device events";
+ for (auto&& tdEvt : toDeviceEvents) {
+ if (processIfVerificationEvent(*tdEvt, false))
+ continue;
+ if (auto&& event = eventCast<EncryptedEvent>(std::move(tdEvt))) {
+ if (event->algorithm() != OlmV1Curve25519AesSha2AlgoKey) {
+ qCDebug(E2EE) << "Unsupported algorithm" << event->id()
+ << "for event" << event->algorithm();
+ return;
+ }
+ if (isKnownCurveKey(event->senderId(), event->senderKey())) {
+ handleEncryptedToDeviceEvent(*event);
+ return;
+ }
+ trackedUsers += event->senderId();
+ outdatedUsers += event->senderId();
+ encryptionUpdateRequired = true;
+ pendingEncryptedEvents.push_back(std::move(event));
+ }
}
- if (is<IgnoredUsersEvent>(*accountEvent))
- qCDebug(MAIN) << "Users ignored by" << d->userId << "updated:"
- << QStringList::fromSet(ignoredUsers()).join(',');
-
- auto& currentData = d->accountData[accountEvent->matrixType()];
- // A polymorphic event-specific comparison might be a bit more
- // efficient; maaybe do it another day
- if (!currentData ||
- currentData->contentJson() != accountEvent->contentJson())
- {
- currentData = std::move(accountEvent);
- qCDebug(MAIN) << "Updated account data of type"
- << currentData->matrixType();
- emit accountDataChanged(currentData->matrixType());
+ }
+#endif
+}
+
+#ifdef Quotient_E2EE_ENABLED
+bool Connection::Private::processIfVerificationEvent(const Event& evt,
+ bool encrypted)
+{
+ return switchOnType(evt,
+ [this, encrypted](const KeyVerificationRequestEvent& reqEvt) {
+ const auto sessionIter = verificationSessions.insert(
+ reqEvt.transactionId(),
+ new KeyVerificationSession(q->userId(), reqEvt, q, encrypted));
+ emit q->newKeyVerificationSession(*sessionIter);
+ return true;
+ },
+ [](const KeyVerificationDoneEvent&) {
+ return true;
+ },
+ [this](const KeyVerificationEvent& kvEvt) {
+ if (auto* const session =
+ verificationSessions.value(kvEvt.transactionId())) {
+ session->handleEvent(kvEvt);
+ emit q->keyVerificationStateChanged(session, session->state());
+ }
+ return true;
+ },
+ false);
+}
+
+void Connection::Private::handleEncryptedToDeviceEvent(const EncryptedEvent& event)
+{
+ const auto [decryptedEvent, olmSessionId] = sessionDecryptMessage(event);
+ if(!decryptedEvent) {
+ qCWarning(E2EE) << "Failed to decrypt event" << event.id();
+ return;
+ }
+
+ if (processIfVerificationEvent(*decryptedEvent, true))
+ return;
+ switchOnType(*decryptedEvent,
+ [this, &event,
+ olmSessionId = olmSessionId](const RoomKeyEvent& roomKeyEvent) {
+ if (auto* detectedRoom = q->room(roomKeyEvent.roomId())) {
+ detectedRoom->handleRoomKeyEvent(roomKeyEvent, event.senderId(),
+ olmSessionId);
+ } else {
+ qCDebug(E2EE)
+ << "Encrypted event room id" << roomKeyEvent.roomId()
+ << "is not found at the connection" << q->objectName();
+ }
+ },
+ [](const Event& evt) {
+ qCWarning(E2EE) << "Skipping encrypted to_device event, type"
+ << evt.matrixType();
+ });
+}
+#endif
+
+void Connection::Private::consumeDevicesList(DevicesList&& devicesList)
+{
+#ifdef Quotient_E2EE_ENABLED
+ bool hasNewOutdatedUser = false;
+ for(const auto &changed : devicesList.changed) {
+ if(trackedUsers.contains(changed)) {
+ outdatedUsers += changed;
+ hasNewOutdatedUser = true;
}
}
+ for(const auto &left : devicesList.left) {
+ trackedUsers -= left;
+ outdatedUsers -= left;
+ deviceKeys.remove(left);
+ }
+ if(hasNewOutdatedUser) {
+ loadOutdatedUserDevices();
+ }
+#endif
}
void Connection::stopSync()
{
- if (d->syncJob)
+ // If there's a sync loop, break it
+ disconnect(d->syncLoopConnection);
+ if (d->syncJob) // If there's an ongoing sync job, stop it too
{
- d->syncJob->abandon();
+ if (d->syncJob->status().code == BaseJob::Pending)
+ d->syncJob->abandon();
d->syncJob = nullptr;
}
}
-PostReceiptJob* Connection::postReceipt(Room* room, RoomEvent* event) const
-{
- return callApi<PostReceiptJob>(room->id(), "m.read", event->id());
-}
+QString Connection::nextBatchToken() const { return d->data->lastEvent(); }
JoinRoomJob* Connection::joinRoom(const QString& roomAlias,
const QStringList& serverNames)
{
- auto job = callApi<JoinRoomJob>(roomAlias, serverNames);
- connect(job, &JoinRoomJob::success,
- this, [this, job] { provideRoom(job->roomId(), JoinState::Join); });
+ auto* const job = callApi<JoinRoomJob>(roomAlias, serverNames);
+ // Upon completion, ensure a room object is created in case it hasn't come
+ // with a sync yet. If the room object is not there, provideRoom() will
+ // create it in Join state. finished() is used here instead of success()
+ // to overtake clients that may add their own slots to finished().
+ connect(job, &BaseJob::finished, this, [this, job] {
+ if (job->status().good())
+ provideRoom(job->roomId());
+ });
return job;
}
-void Connection::leaveRoom(Room* room)
+LeaveRoomJob* Connection::leaveRoom(Room* room)
{
- callApi<LeaveRoomJob>(room->id());
+ const auto& roomId = room->id();
+ const auto job = callApi<LeaveRoomJob>(roomId);
+ if (room->joinState() == JoinState::Invite) {
+ // Workaround matrix-org/synapse#2181 - if the room is in invite state
+ // the invite may have been cancelled but Synapse didn't send it in
+ // `/sync`. See also #273 for the discussion in the library context.
+ d->pendingStateRoomIds.push_back(roomId);
+ connect(job, &LeaveRoomJob::success, this, [this, roomId] {
+ if (d->pendingStateRoomIds.removeOne(roomId)) {
+ qCDebug(MAIN) << "Forcing the room to Leave status";
+ provideRoom(roomId, JoinState::Leave);
+ }
+ });
+ }
+ return job;
}
inline auto splitMediaId(const QString& mediaId)
{
auto idParts = mediaId.split('/');
Q_ASSERT_X(idParts.size() == 2, __FUNCTION__,
- ("'" + mediaId +
- "' doesn't look like 'serverName/localMediaId'").toLatin1());
+ ("'" + mediaId + "' doesn't look like 'serverName/localMediaId'")
+ .toLatin1());
return idParts;
}
+QUrl Connection::makeMediaUrl(QUrl mxcUrl) const
+{
+ Q_ASSERT(mxcUrl.scheme() == "mxc");
+ QUrlQuery q(mxcUrl.query());
+ q.addQueryItem(QStringLiteral("user_id"), userId());
+ mxcUrl.setQuery(q);
+ return mxcUrl;
+}
+
MediaThumbnailJob* Connection::getThumbnail(const QString& mediaId,
- QSize requestedSize, RunningPolicy policy) const
+ QSize requestedSize,
+ RunningPolicy policy)
{
auto idParts = splitMediaId(mediaId);
- return callApi<MediaThumbnailJob>(policy,
- idParts.front(), idParts.back(), requestedSize);
+ return callApi<MediaThumbnailJob>(policy, idParts.front(), idParts.back(),
+ requestedSize);
}
-MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
- QSize requestedSize, RunningPolicy policy) const
+MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, QSize requestedSize,
+ RunningPolicy policy)
{
return getThumbnail(url.authority() + url.path(), requestedSize, policy);
}
-MediaThumbnailJob* Connection::getThumbnail(const QUrl& url,
- int requestedWidth, int requestedHeight, RunningPolicy policy) const
+MediaThumbnailJob* Connection::getThumbnail(const QUrl& url, int requestedWidth,
+ int requestedHeight,
+ RunningPolicy policy)
{
return getThumbnail(url, QSize(requestedWidth, requestedHeight), policy);
}
-UploadContentJob* Connection::uploadContent(QIODevice* contentSource,
- const QString& filename, const QString& contentType) const
+UploadContentJob*
+Connection::uploadContent(QIODevice* contentSource, const QString& filename,
+ const QString& overrideContentType)
{
+ Q_ASSERT(contentSource != nullptr);
+ auto contentType = overrideContentType;
+ if (contentType.isEmpty()) {
+ contentType = QMimeDatabase()
+ .mimeTypeForFileNameAndData(filename, contentSource)
+ .name();
+ if (!contentSource->open(QIODevice::ReadOnly)) {
+ qCWarning(MAIN) << "Couldn't open content source" << filename
+ << "for reading:" << contentSource->errorString();
+ return nullptr;
+ }
+ }
return callApi<UploadContentJob>(contentSource, filename, contentType);
}
UploadContentJob* Connection::uploadFile(const QString& fileName,
- const QString& contentType)
+ const QString& overrideContentType)
{
auto sourceFile = new QFile(fileName);
- if (!sourceFile->open(QIODevice::ReadOnly))
- {
- qCWarning(MAIN) << "Couldn't open" << sourceFile->fileName()
- << "for reading";
- return nullptr;
- }
return uploadContent(sourceFile, QFileInfo(*sourceFile).fileName(),
- contentType);
+ overrideContentType);
}
-GetContentJob* Connection::getContent(const QString& mediaId) const
+GetContentJob* Connection::getContent(const QString& mediaId)
{
auto idParts = splitMediaId(mediaId);
return callApi<GetContentJob>(idParts.front(), idParts.back());
}
-GetContentJob* Connection::getContent(const QUrl& url) const
+GetContentJob* Connection::getContent(const QUrl& url)
{
return getContent(url.authority() + url.path());
}
DownloadFileJob* Connection::downloadFile(const QUrl& url,
- const QString& localFilename) const
+ const QString& localFilename)
{
auto mediaId = url.authority() + url.path();
auto idParts = splitMediaId(mediaId);
- auto* job = callApi<DownloadFileJob>(idParts.front(), idParts.back(),
- localFilename);
+ auto* job =
+ callApi<DownloadFileJob>(idParts.front(), idParts.back(), localFilename);
return job;
}
-CreateRoomJob* Connection::createRoom(RoomVisibility visibility,
- const QString& alias, const QString& name, const QString& topic,
- QStringList invites, const QString& presetName, bool isDirect,
- const QVector<CreateRoomJob::StateEvent>& initialState,
- const QVector<CreateRoomJob::Invite3pid>& invite3pids,
- const QJsonObject& creationContent)
-{
- invites.removeOne(d->userId); // The creator is by definition in the room
- auto job = callApi<CreateRoomJob>(
- visibility == PublishRoom ? QStringLiteral("public")
- : QStringLiteral("private"),
- alias, name, topic, invites, invite3pids, QString(/*TODO: #233*/),
- creationContent, initialState, presetName, isDirect);
- connect(job, &BaseJob::success, this, [this,job] {
- emit createdRoom(provideRoom(job->roomId(), JoinState::Join));
+#ifdef Quotient_E2EE_ENABLED
+DownloadFileJob* Connection::downloadFile(
+ const QUrl& url, const EncryptedFileMetadata& fileMetadata,
+ const QString& localFilename)
+{
+ auto mediaId = url.authority() + url.path();
+ auto idParts = splitMediaId(mediaId);
+ return callApi<DownloadFileJob>(idParts.front(), idParts.back(),
+ fileMetadata, localFilename);
+}
+#endif
+
+CreateRoomJob*
+Connection::createRoom(RoomVisibility visibility, const QString& alias,
+ const QString& name, const QString& topic,
+ QStringList invites, const QString& presetName,
+ const QString& roomVersion, bool isDirect,
+ const QVector<CreateRoomJob::StateEvent>& initialState,
+ const QVector<CreateRoomJob::Invite3pid>& invite3pids,
+ const QJsonObject& creationContent)
+{
+ invites.removeOne(userId()); // The creator is by definition in the room
+ auto job = callApi<CreateRoomJob>(visibility == PublishRoom
+ ? QStringLiteral("public")
+ : QStringLiteral("private"),
+ alias, name, topic, invites, invite3pids,
+ roomVersion, creationContent,
+ initialState, presetName, isDirect);
+ connect(job, &BaseJob::success, this, [this, job, invites, isDirect] {
+ auto* room = provideRoom(job->roomId(), JoinState::Join);
+ if (!room) {
+ Q_ASSERT_X(room, "Connection::createRoom",
+ "Failed to create a room");
+ return;
+ }
+ emit createdRoom(room);
+ if (isDirect)
+ for (const auto& i : invites)
+ addToDirectChats(room, user(i));
});
return job;
}
void Connection::requestDirectChat(const QString& userId)
{
- if (auto* u = user(userId))
- requestDirectChat(u);
- else
- qCCritical(MAIN)
- << "Connection::requestDirectChat: Couldn't get a user object for"
- << userId;
+ doInDirectChat(userId, [this](Room* r) { emit directChatAvailable(r); });
}
void Connection::requestDirectChat(User* u)
{
- doInDirectChat(u, [this] (Room* r) { emit directChatAvailable(r); });
+ doInDirectChat(u, [this](Room* r) { emit directChatAvailable(r); });
}
void Connection::doInDirectChat(const QString& userId,
@@ -555,34 +1270,33 @@ void Connection::doInDirectChat(User* u,
const std::function<void(Room*)>& operation)
{
Q_ASSERT(u);
- const auto& userId = u->id();
- // There can be more than one DC; find the first valid, and delete invalid
- // (left/forgotten) ones along the way.
+ const auto& otherUserId = u->id();
+ // There can be more than one DC; find the first valid (existing and
+ // not left), and delete inexistent (forgotten?) ones along the way.
DirectChatsMap removals;
- for (auto it = d->directChats.find(u);
- it != d->directChats.end() && it.key() == u; ++it)
- {
+ for (auto it = d->directChats.constFind(u);
+ it != d->directChats.cend() && it.key() == u; ++it) {
const auto& roomId = *it;
- if (auto r = room(roomId, JoinState::Join))
- {
+ if (auto r = room(roomId, JoinState::Join)) {
Q_ASSERT(r->id() == roomId);
// A direct chat with yourself should only involve yourself :)
- if (userId == d->userId && r->memberCount() > 1)
+ if (otherUserId == userId() && r->totalMemberCount() > 1)
continue;
- qCDebug(MAIN) << "Requested direct chat with" << userId
+ qCDebug(MAIN) << "Requested direct chat with" << otherUserId
<< "is already available as" << r->id();
operation(r);
return;
}
- if (auto ir = invitation(roomId))
- {
+ if (auto ir = invitation(roomId)) {
Q_ASSERT(ir->id() == roomId);
auto j = joinRoom(ir->id());
- connect(j, &BaseJob::success, this, [this,roomId,userId,operation] {
- qCDebug(MAIN) << "Joined the already invited direct chat with"
- << userId << "as" << roomId;
- operation(room(roomId, JoinState::Join));
- });
+ connect(j, &BaseJob::success, this,
+ [this, roomId, otherUserId, operation] {
+ qCDebug(MAIN)
+ << "Joined the already invited direct chat with"
+ << otherUserId << "as" << roomId;
+ operation(room(roomId, JoinState::Join));
+ });
return;
}
// Avoid reusing previously left chats but don't remove them
@@ -590,36 +1304,36 @@ void Connection::doInDirectChat(User* u,
if (room(roomId, JoinState::Leave))
continue;
- qCWarning(MAIN) << "Direct chat with" << userId << "known as room"
+ qCWarning(MAIN) << "Direct chat with" << otherUserId << "known as room"
<< roomId << "is not valid and will be discarded";
// Postpone actual deletion until we finish iterating d->directChats.
removals.insert(it.key(), it.value());
+ // Add to the list of updates to send to the server upon the next sync.
+ d->dcLocalRemovals.insert(it.key(), it.value());
}
- if (!removals.isEmpty())
- {
- for (auto it = removals.cbegin(); it != removals.cend(); ++it)
- {
+ if (!removals.isEmpty()) {
+ for (auto it = removals.cbegin(); it != removals.cend(); ++it) {
d->directChats.remove(it.key(), it.value());
d->directChatUsers.remove(it.value(),
const_cast<User*>(it.key())); // FIXME
}
- d->broadcastDirectChatUpdates({}, removals);
+ emit directChatsListChanged({}, removals);
}
- auto j = createDirectChat(userId);
- connect(j, &BaseJob::success, this, [this,j,userId,operation] {
- qCDebug(MAIN) << "Direct chat with" << userId
- << "has been created as" << j->roomId();
+ auto j = createDirectChat(otherUserId);
+ connect(j, &BaseJob::success, this, [this, j, otherUserId, operation] {
+ qCDebug(MAIN) << "Direct chat with" << otherUserId << "has been created as"
+ << j->roomId();
operation(room(j->roomId(), JoinState::Join));
});
-
}
CreateRoomJob* Connection::createDirectChat(const QString& userId,
- const QString& topic, const QString& name)
+ const QString& topic,
+ const QString& name)
{
- return createRoom(UnpublishRoom, "", name, topic, {userId},
- "trusted_private_chat", true);
+ return createRoom(UnpublishRoom, {}, name, topic, { userId },
+ QStringLiteral("trusted_private_chat"), {}, true);
}
ForgetRoomJob* Connection::forgetRoom(const QString& id)
@@ -632,164 +1346,212 @@ ForgetRoomJob* Connection::forgetRoom(const QString& id)
// a ForgetRoomJob is created in advance and can be returned in a probably
// not-yet-started state (it will start once /leave completes).
auto forgetJob = new ForgetRoomJob(id);
- auto room = d->roomMap.value({id, false});
+ auto room = d->roomMap.value({ id, false });
if (!room)
- room = d->roomMap.value({id, true});
- if (room && room->joinState() != JoinState::Leave)
- {
- auto leaveJob = room->leaveRoom();
- connect(leaveJob, &BaseJob::success, this, [this, forgetJob, room] {
- forgetJob->start(connectionData());
- // If the matching /sync response hasn't arrived yet, mark the room
- // for explicit deletion
- if (room->joinState() != JoinState::Leave)
- d->roomIdsToForget.push_back(room->id());
- });
+ room = d->roomMap.value({ id, true });
+ if (room && room->joinState() != JoinState::Leave) {
+ auto leaveJob = leaveRoom(room);
+ connect(leaveJob, &BaseJob::result, this,
+ [this, leaveJob, forgetJob, room] {
+ if (leaveJob->error() == BaseJob::Success
+ || leaveJob->error() == BaseJob::NotFound) {
+ run(forgetJob);
+ // If the matching /sync response hasn't arrived yet,
+ // mark the room for explicit deletion
+ if (room->joinState() != JoinState::Leave)
+ d->roomIdsToForget.push_back(room->id());
+ } else {
+ qCWarning(MAIN).nospace()
+ << "Error leaving room " << room->objectName()
+ << ": " << leaveJob->errorString();
+ forgetJob->abandon();
+ }
+ });
connect(leaveJob, &BaseJob::failure, forgetJob, &BaseJob::abandon);
- }
- else
- forgetJob->start(connectionData());
- connect(forgetJob, &BaseJob::success, this, [this, id]
- {
- // Delete whatever instances of the room are still in the map.
- for (auto f: {false, true})
- if (auto r = d->roomMap.take({ id, f }))
- {
- qCDebug(MAIN) << "Room" << r->objectName()
- << "in state" << toCString(r->joinState())
- << "will be deleted";
- emit r->beforeDestruction(r);
- r->deleteLater();
- }
+ } else
+ run(forgetJob);
+ connect(forgetJob, &BaseJob::result, this, [this, id, forgetJob] {
+ // Leave room in case of success, or room not known by server
+ if (forgetJob->error() == BaseJob::Success
+ || forgetJob->error() == BaseJob::NotFound)
+ d->removeRoom(id); // Delete the room from roomMap
+ else
+ qCWarning(MAIN).nospace() << "Error forgetting room " << id << ": "
+ << forgetJob->errorString();
});
return forgetJob;
}
-SendToDeviceJob* Connection::sendToDevices(const QString& eventType,
- const UsersToDevicesToEvents& eventsMap) const
+SendToDeviceJob* Connection::sendToDevices(
+ const QString& eventType, const UsersToDevicesToContent& contents)
{
- QHash<QString, QHash<QString, QJsonObject>> json;
- json.reserve(int(eventsMap.size()));
- std::for_each(eventsMap.begin(), eventsMap.end(),
- [&json] (const auto& userTodevicesToEvents) {
- auto& jsonUser = json[userTodevicesToEvents.first];
- const auto& devicesToEvents = userTodevicesToEvents.second;
- std::for_each(devicesToEvents.begin(), devicesToEvents.end(),
- [&jsonUser] (const auto& deviceToEvents) {
- jsonUser.insert(deviceToEvents.first,
- deviceToEvents.second.contentJson());
- });
- });
- return callApi<SendToDeviceJob>(BackgroundRequest,
- eventType, generateTxnId(), json);
+ return callApi<SendToDeviceJob>(BackgroundRequest, eventType,
+ generateTxnId(), contents);
}
SendMessageJob* Connection::sendMessage(const QString& roomId,
- const RoomEvent& event) const
+ const RoomEvent& event)
+{
+ const auto txnId = event.transactionId().isEmpty() ? generateTxnId()
+ : event.transactionId();
+ return callApi<SendMessageJob>(roomId, event.matrixType(), txnId,
+ event.contentJson());
+}
+
+QUrl Connection::homeserver() const { return d->data->baseUrl(); }
+
+QString Connection::domain() const { return userId().section(':', 1); }
+
+bool Connection::isUsable() const { return !loginFlows().isEmpty(); }
+
+QVector<GetLoginFlowsJob::LoginFlow> Connection::loginFlows() const
{
- const auto txnId = event.transactionId().isEmpty()
- ? generateTxnId() : event.transactionId();
- return callApi<SendMessageJob>(roomId, event.matrixType(),
- txnId, event.contentJson());
+ return d->loginFlows;
}
-QUrl Connection::homeserver() const
+bool Connection::supportsPasswordAuth() const
{
- return d->data->baseUrl();
+ return d->loginFlows.contains(LoginFlows::Password);
+}
+
+bool Connection::supportsSso() const
+{
+ return d->loginFlows.contains(LoginFlows::SSO);
}
Room* Connection::room(const QString& roomId, JoinStates states) const
{
- Room* room = d->roomMap.value({roomId, false}, nullptr);
- if (states.testFlag(JoinState::Join) &&
- room && room->joinState() == JoinState::Join)
+ Room* room = d->roomMap.value({ roomId, false }, nullptr);
+ if (states.testFlag(JoinState::Join) && room
+ && room->joinState() == JoinState::Join)
return room;
if (states.testFlag(JoinState::Invite))
if (Room* invRoom = invitation(roomId))
return invRoom;
- if (states.testFlag(JoinState::Leave) &&
- room && room->joinState() == JoinState::Leave)
+ if (states.testFlag(JoinState::Leave) && room
+ && room->joinState() == JoinState::Leave)
return room;
return nullptr;
}
+Room* Connection::roomByAlias(const QString& roomAlias, JoinStates states) const
+{
+ const auto id = d->roomAliasMap.value(roomAlias);
+ if (!id.isEmpty())
+ return room(id, states);
+
+ qCWarning(MAIN) << "Room for alias" << roomAlias
+ << "is not found under account" << userId();
+ return nullptr;
+}
+
+void Connection::updateRoomAliases(const QString& roomId,
+ const QStringList& previousRoomAliases,
+ const QStringList& roomAliases)
+{
+ for (const auto& a : previousRoomAliases)
+ if (d->roomAliasMap.remove(a) == 0)
+ qCWarning(MAIN) << "Alias" << a << "is not found (already deleted?)";
+
+ for (const auto& a : roomAliases) {
+ auto& mappedId = d->roomAliasMap[a];
+ if (!mappedId.isEmpty()) {
+ if (mappedId == roomId)
+ qCDebug(MAIN)
+ << "Alias" << a << "is already mapped to" << roomId;
+ else
+ qCWarning(MAIN) << "Alias" << a << "will be force-remapped from"
+ << mappedId << "to" << roomId;
+ }
+ mappedId = roomId;
+ }
+}
+
Room* Connection::invitation(const QString& roomId) const
{
- return d->roomMap.value({roomId, true}, nullptr);
+ return d->roomMap.value({ roomId, true }, nullptr);
}
-User* Connection::user(const QString& userId)
+User* Connection::user(const QString& uId)
{
- if (userId.isEmpty())
+ if (uId.isEmpty())
return nullptr;
- if (!userId.startsWith('@') || !userId.contains(':'))
- {
- qCCritical(MAIN) << "Malformed userId:" << userId;
+ if (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(userId) )
- return d->userMap.value(userId);
- auto* user = userFactory()(this, userId);
- d->userMap.insert(userId, user);
+ auto* user = userFactory()(this, uId);
+ d->userMap.insert(uId, user);
emit newUser(user);
return user;
}
const User* Connection::user() const
{
- return d->userMap.value(d->userId, nullptr);
+ return d->userMap.value(userId(), nullptr);
}
-User* Connection::user()
-{
- return user(d->userId);
-}
+User* Connection::user() { return user(userId()); }
-QString Connection::userId() const
-{
- return d->userId;
-}
+QString Connection::userId() const { return d->data->userId(); }
-QString Connection::deviceId() const
+QString Connection::deviceId() const { return d->data->deviceId(); }
+
+QByteArray Connection::accessToken() const
{
- return d->data->deviceId();
+ // The logout job needs access token to do its job; so the token is
+ // kept inside d->data but no more exposed to the outside world.
+ return isJobPending(d->logoutJob) ? QByteArray() : d->data->accessToken();
}
-QString Connection::token() const
+bool Connection::isLoggedIn() const { return !accessToken().isEmpty(); }
+
+#ifdef Quotient_E2EE_ENABLED
+QOlmAccount *Connection::olmAccount() const
{
- return accessToken();
+ return d->olmAccount.get();
}
+#endif // Quotient_E2EE_ENABLED
-QByteArray Connection::accessToken() const
+SyncJob* Connection::syncJob() const { return d->syncJob; }
+
+int Connection::millisToReconnect() const
{
- return d->data->accessToken();
+ return d->syncJob ? d->syncJob->millisToRetry() : 0;
}
-SyncJob* Connection::syncJob() const
+QVector<Room*> Connection::allRooms() const
{
- return d->syncJob;
+ QVector<Room*> result;
+ result.resize(d->roomMap.size());
+ std::copy(d->roomMap.cbegin(), d->roomMap.cend(), result.begin());
+ return result;
}
-int Connection::millisToReconnect() const
+QVector<Room*> Connection::rooms(JoinStates joinStates) const
{
- return d->syncJob ? d->syncJob->millisToRetry() : 0;
+ QVector<Room*> result;
+ for (auto* r: qAsConst(d->roomMap))
+ if (joinStates.testFlag(r->joinState()))
+ result.push_back(r);
+ return result;
}
-QHash< QPair<QString, bool>, Room* > Connection::roomMap() const
+int Connection::roomsCount(JoinStates joinStates) const
{
- // Copy-on-write-and-remove-elements is faster than copying elements one by one.
- QHash< QPair<QString, bool>, Room* > roomMap = d->roomMap;
- for (auto it = roomMap.begin(); it != roomMap.end(); )
- {
- if (it.value()->joinState() == JoinState::Leave)
- it = roomMap.erase(it);
- else
- ++it;
- }
- return roomMap;
+ // Using int to maintain compatibility with QML
+ // (consider also that QHash<>::size() returns int anyway).
+ return int(std::count_if(d->roomMap.cbegin(), d->roomMap.cend(),
+ [joinStates](Room* r) {
+ return joinStates.testFlag(r->joinState());
+ }));
}
bool Connection::hasAccountData(const QString& type) const
@@ -823,26 +1585,27 @@ void Connection::setAccountData(const QString& type, const QJsonObject& content)
QHash<QString, QVector<Room*>> Connection::tagsToRooms() const
{
QHash<QString, QVector<Room*>> result;
- for (auto* r: qAsConst(d->roomMap))
- {
- for (const auto& tagName: r->tagNames())
+ for (auto* r : qAsConst(d->roomMap)) {
+ const auto& tagNames = r->tagNames();
+ for (const auto& tagName : tagNames)
result[tagName].push_back(r);
}
for (auto it = result.begin(); it != result.end(); ++it)
- std::sort(it->begin(), it->end(),
- [t=it.key()] (Room* r1, Room* r2) {
- return r1->tags().value(t) < r2->tags().value(t);
- });
+ std::sort(it->begin(), it->end(), [t = it.key()](Room* r1, Room* r2) {
+ return r1->tags().value(t) < r2->tags().value(t);
+ });
return result;
}
QStringList Connection::tagNames() const
{
- QStringList tags ({FavouriteTag});
- for (auto* r: qAsConst(d->roomMap))
- for (const auto& tag: r->tagNames())
+ QStringList tags({ FavouriteTag });
+ for (auto* r : qAsConst(d->roomMap)) {
+ const auto& tagNames = r->tagNames();
+ for (const auto& tag : tagNames)
if (tag != LowPriorityTag && !tags.contains(tag))
tags.push_back(tag);
+ }
tags.push_back(LowPriorityTag);
return tags;
}
@@ -850,36 +1613,27 @@ QStringList Connection::tagNames() const
QVector<Room*> Connection::roomsWithTag(const QString& tagName) const
{
QVector<Room*> rooms;
- std::copy_if(d->roomMap.begin(), d->roomMap.end(), std::back_inserter(rooms),
- [&tagName] (Room* r) { return r->tags().contains(tagName); });
+ std::copy_if(d->roomMap.cbegin(), d->roomMap.cend(),
+ std::back_inserter(rooms),
+ [&tagName](Room* r) { return r->tags().contains(tagName); });
return rooms;
}
-Connection::DirectChatsMap Connection::directChats() const
+DirectChatsMap Connection::directChats() const
{
return d->directChats;
}
-QJsonObject toJson(const Connection::DirectChatsMap& directChats)
-{
- QJsonObject json;
- for (auto it = directChats.begin(); it != directChats.end();)
- {
- QJsonArray roomIds;
- const auto* user = it.key();
- for (; it != directChats.end() && it.key() == user; ++it)
- roomIds.append(*it);
- json.insert(user->id(), roomIds);
- }
- return json;
-}
-
-void Connection::Private::broadcastDirectChatUpdates(const DirectChatsMap& additions,
- const DirectChatsMap& removals)
+// Removes room with given id from roomMap
+void Connection::Private::removeRoom(const QString& roomId)
{
- q->callApi<SetAccountDataJob>(userId, QStringLiteral("m.direct"),
- toJson(directChats));
- emit q->directChatsListChanged(additions, removals);
+ for (auto f : { false, true })
+ if (auto r = roomMap.take({ roomId, f })) {
+ qCDebug(MAIN) << "Room" << r->objectName() << "in state" << terse
+ << r->joinState() << "will be deleted";
+ emit r->beforeDestruction(r);
+ r->deleteLater();
+ }
}
void Connection::addToDirectChats(const Room* room, User* user)
@@ -890,29 +1644,30 @@ void Connection::addToDirectChats(const Room* room, User* user)
Q_ASSERT(!d->directChatUsers.contains(room->id(), user));
d->directChats.insert(user, room->id());
d->directChatUsers.insert(room->id(), user);
- DirectChatsMap additions { { user, room->id() } };
- d->broadcastDirectChatUpdates(additions, {});
+ d->dcLocalAdditions.insert(user, room->id());
+ emit directChatsListChanged({ { user, room->id() } }, {});
}
void Connection::removeFromDirectChats(const QString& roomId, User* user)
{
Q_ASSERT(!roomId.isEmpty());
- if ((user != nullptr && !d->directChats.contains(user, roomId)) ||
- d->directChats.key(roomId) == nullptr)
+ if ((user != nullptr && !d->directChats.contains(user, roomId))
+ || d->directChats.key(roomId) == nullptr)
return;
DirectChatsMap removals;
- if (user != nullptr)
- {
- removals.insert(user, roomId);
+ if (user != nullptr) {
d->directChats.remove(user, roomId);
d->directChatUsers.remove(roomId, user);
+ removals.insert(user, roomId);
+ d->dcLocalRemovals.insert(user, roomId);
} else {
- removals = erase_if(d->directChats,
- [&roomId] (auto it) { return it.value() == roomId; });
+ removals = remove_if(d->directChats,
+ [&roomId](auto it) { return it.value() == roomId; });
d->directChatUsers.remove(roomId);
+ d->dcLocalRemovals += removals;
}
- d->broadcastDirectChatUpdates({}, removals);
+ emit directChatsListChanged({}, removals);
}
bool Connection::isDirectChat(const QString& roomId) const
@@ -931,10 +1686,10 @@ bool Connection::isIgnored(const User* user) const
return ignoredUsers().contains(user->id());
}
-Connection::IgnoredUsersList Connection::ignoredUsers() const
+IgnoredUsersList Connection::ignoredUsers() const
{
- const auto* event = d->unpackAccountData<IgnoredUsersEvent>();
- return event ? event->ignored_users() : IgnoredUsersList();
+ const auto* event = accountData<IgnoredUsersEvent>();
+ return event ? event->ignoredUsers() : IgnoredUsersList();
}
void Connection::addToIgnoredUsers(const User* user)
@@ -942,11 +1697,10 @@ void Connection::addToIgnoredUsers(const User* user)
Q_ASSERT(user != nullptr);
auto ignoreList = ignoredUsers();
- if (!ignoreList.contains(user->id()))
- {
+ if (!ignoreList.contains(user->id())) {
ignoreList.insert(user->id());
d->packAndSendAccountData<IgnoredUsersEvent>(ignoreList);
- emit ignoredUsersListChanged({{ user->id() }}, {});
+ emit ignoredUsersListChanged({ { user->id() } }, {});
}
}
@@ -955,70 +1709,81 @@ void Connection::removeFromIgnoredUsers(const User* user)
Q_ASSERT(user != nullptr);
auto ignoreList = ignoredUsers();
- if (ignoreList.remove(user->id()) != 0)
- {
+ if (ignoreList.remove(user->id()) != 0) {
d->packAndSendAccountData<IgnoredUsersEvent>(ignoreList);
- emit ignoredUsersListChanged({}, {{ user->id() }});
+ emit ignoredUsersListChanged({}, { { user->id() } });
}
}
-QMap<QString, User*> Connection::users() const
-{
- return d->userMap;
-}
+QMap<QString, User*> Connection::users() const { return d->userMap; }
const ConnectionData* Connection::connectionData() const
{
return d->data.get();
}
-Room* Connection::provideRoom(const QString& id, JoinState joinState)
+Room* Connection::provideRoom(const QString& id, Omittable<JoinState> joinState)
{
// TODO: This whole function is a strong case for a RoomManager class.
Q_ASSERT_X(!id.isEmpty(), __FUNCTION__, "Empty room id");
- const auto roomKey = qMakePair(id, joinState == JoinState::Invite);
+ // If joinState is empty, all joinState == comparisons below are false.
+ const std::pair roomKey { id, joinState == JoinState::Invite };
auto* room = d->roomMap.value(roomKey, nullptr);
- if (room)
- {
+ if (room) {
// Leave is a special case because in transition (5a) (see the .h file)
// joinState == room->joinState but we still have to preempt the Invite
// and emit a signal. For Invite and Join, there's no such problem.
if (room->joinState() == joinState && joinState != JoinState::Leave)
return room;
+ } else if (!joinState) {
+ // No Join and Leave, maybe Invite?
+ room = d->roomMap.value({ id, true }, nullptr);
+ if (room)
+ return room;
+ // No Invite either, setup a new room object in Join state
+ joinState = JoinState::Join;
}
- else
- {
- room = roomFactory()(this, id, joinState);
- if (!room)
- {
+
+ if (!room) {
+ Q_ASSERT(joinState.has_value());
+ room = roomFactory()(this, id, *joinState);
+ if (!room) {
qCCritical(MAIN) << "Failed to create a room" << id;
return nullptr;
}
d->roomMap.insert(roomKey, room);
- d->firstTimeRooms.push_back(room);
- connect(room, &Room::beforeDestruction,
- this, &Connection::aboutToDeleteRoom);
+ connect(room, &Room::beforeDestruction, this,
+ &Connection::aboutToDeleteRoom);
+ 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 == JoinState::Invite)
- {
+ if (!joinState)
+ return room;
+
+ if (*joinState == JoinState::Invite) {
// prev is either Leave or nullptr
- auto* prev = d->roomMap.value({id, false}, nullptr);
+ auto* prev = d->roomMap.value({ id, false }, nullptr);
emit invitedRoom(room, prev);
- }
- else
- {
- room->setJoinState(joinState);
+ } else {
+ room->setJoinState(*joinState);
// Preempt the Invite room (if any) with a room in Join/Leave state.
- auto* prevInvite = d->roomMap.take({id, true});
- if (joinState == JoinState::Join)
+ auto* prevInvite = d->roomMap.take({ id, true });
+ if (*joinState == JoinState::Join)
emit joinedRoom(room, prevInvite);
- else if (joinState == JoinState::Leave)
+ else if (*joinState == JoinState::Leave)
emit leftRoom(room, prevInvite);
- if (prevInvite)
- {
- qCDebug(MAIN) << "Deleting Invite state for room" << prevInvite->id();
+ if (prevInvite) {
+ const auto dcUsers = prevInvite->directChatUsers();
+ for (auto* u : dcUsers)
+ addToDirectChats(room, u);
+ qCDebug(MAIN) << "Deleting Invite state for room"
+ << prevInvite->id();
emit prevInvite->beforeDestruction(prevInvite);
prevInvite->deleteLater();
}
@@ -1037,18 +1802,12 @@ void Connection::setUserFactory(user_factory_t f)
_userFactory = std::move(f);
}
-room_factory_t Connection::roomFactory()
-{
- return _roomFactory;
-}
+room_factory_t Connection::roomFactory() { return _roomFactory; }
-user_factory_t Connection::userFactory()
-{
- return _userFactory;
-}
+user_factory_t Connection::userFactory() { return _userFactory; }
-room_factory_t Connection::_roomFactory = defaultRoomFactory<>();
-user_factory_t Connection::_userFactory = defaultUserFactory<>();
+room_factory_t Connection::_roomFactory = defaultRoomFactory<>;
+user_factory_t Connection::_userFactory = defaultUserFactory<>;
QByteArray Connection::generateTxnId() const
{
@@ -1057,163 +1816,672 @@ QByteArray Connection::generateTxnId() const
void Connection::setHomeserver(const QUrl& url)
{
- if (homeserver() == url)
- return;
+ if (isJobPending(d->resolverJob))
+ d->resolverJob->abandon();
+ if (isJobPending(d->loginFlowsJob))
+ d->loginFlowsJob->abandon();
+ d->loginFlows.clear();
- d->data->setBaseUrl(url);
- emit homeserverChanged(homeserver());
-}
+ if (homeserver() != url) {
+ d->data->setBaseUrl(url);
+ emit homeserverChanged(homeserver());
+ }
-static constexpr int CACHE_VERSION_MAJOR = 8;
-static constexpr int CACHE_VERSION_MINOR = 0;
+ // Whenever a homeserver is updated, retrieve available login flows from it
+ d->loginFlowsJob = callApi<GetLoginFlowsJob>(BackgroundRequest);
+ connect(d->loginFlowsJob, &BaseJob::result, this, [this] {
+ if (d->loginFlowsJob->status().good())
+ d->loginFlows = d->loginFlowsJob->flows();
+ else
+ d->loginFlows.clear();
+ emit loginFlowsChanged();
+ });
+}
-void Connection::saveState(const QUrl &toFile) const
+void Connection::saveRoomState(Room* r) const
{
+ Q_ASSERT(r);
if (!d->cacheState)
return;
- QElapsedTimer et; et.start();
+ QFile outRoomFile { stateCacheDir().filePath(
+ SyncData::fileNameForRoom(r->id())) };
+ if (outRoomFile.open(QFile::WriteOnly)) {
+ const auto data =
+ d->cacheToBinary
+ ? QCborValue::fromJsonValue(r->toJson()).toCbor()
+ : QJsonDocument(r->toJson()).toJson(QJsonDocument::Compact);
+ outRoomFile.write(data.data(), data.size());
+ qCDebug(MAIN) << "Room state cache saved to" << outRoomFile.fileName();
+ } else {
+ qCWarning(MAIN) << "Error opening" << outRoomFile.fileName() << ":"
+ << outRoomFile.errorString();
+ }
+}
- QFileInfo stateFile {
- toFile.isEmpty() ? stateCachePath() : toFile.toLocalFile()
- };
- if (!stateFile.dir().exists())
- stateFile.dir().mkpath(".");
+void Connection::saveState() const
+{
+ if (!d->cacheState)
+ return;
- QFile outfile { stateFile.absoluteFilePath() };
- if (!outfile.open(QFile::WriteOnly))
- {
- qCWarning(MAIN) << "Error opening" << stateFile.absoluteFilePath()
- << ":" << outfile.errorString();
+ QElapsedTimer et;
+ et.start();
+
+ QFile outFile { d->topLevelStatePath() };
+ if (!outFile.open(QFile::WriteOnly)) {
+ qCWarning(MAIN) << "Error opening" << outFile.fileName() << ":"
+ << outFile.errorString();
qCWarning(MAIN) << "Caching the rooms state disabled";
d->cacheState = false;
return;
}
- QJsonObject rootObj;
+ QJsonObject rootObj {
+ { QStringLiteral("cache_version"),
+ QJsonObject {
+ { QStringLiteral("major"), SyncData::cacheVersion().first },
+ { QStringLiteral("minor"), SyncData::cacheVersion().second } } }
+ };
{
- QJsonObject rooms;
- QJsonObject inviteRooms;
- for (const auto* i : roomMap()) // Pass on rooms in Leave state
- {
- if (i->joinState() == JoinState::Invite)
- inviteRooms.insert(i->id(), i->toJson());
- else
- rooms.insert(i->id(), i->toJson());
- QElapsedTimer et1; et1.start();
- QCoreApplication::processEvents();
- if (et1.elapsed() > 1)
- qCDebug(PROFILER) << "processEvents() borrowed" << et1;
+ QJsonObject roomsJson;
+ QJsonObject inviteRoomsJson;
+ for (const auto* r: qAsConst(d->roomMap)) {
+ if (r->joinState() == JoinState::Leave)
+ continue;
+ (r->joinState() == JoinState::Invite ? inviteRoomsJson : roomsJson)
+ .insert(r->id(), QJsonValue::Null);
}
QJsonObject roomObj;
- if (!rooms.isEmpty())
- roomObj.insert("join", rooms);
- if (!inviteRooms.isEmpty())
- roomObj.insert("invite", inviteRooms);
+ if (!roomsJson.isEmpty())
+ roomObj.insert(QStringLiteral("join"), roomsJson);
+ if (!inviteRoomsJson.isEmpty())
+ roomObj.insert(QStringLiteral("invite"), inviteRoomsJson);
- rootObj.insert("next_batch", d->data->lastEvent());
- rootObj.insert("rooms", roomObj);
+ rootObj.insert(QStringLiteral("next_batch"), d->data->lastEvent());
+ rootObj.insert(QStringLiteral("rooms"), roomObj);
}
{
QJsonArray accountDataEvents {
- basicEventJson(QStringLiteral("m.direct"), toJson(d->directChats))
+ Event::basicJson(QStringLiteral("m.direct"), toJson(d->directChats))
};
- for (const auto &e : d->accountData)
+ for (const auto& e : d->accountData)
accountDataEvents.append(
- basicEventJson(e.first, e.second->contentJson()));
+ Event::basicJson(e.first, e.second->contentJson()));
- rootObj.insert("account_data",
- QJsonObject {{ QStringLiteral("events"), accountDataEvents }});
+ rootObj.insert(QStringLiteral("account_data"),
+ QJsonObject {
+ { QStringLiteral("events"), accountDataEvents } });
}
+#ifdef Quotient_E2EE_ENABLED
+ {
+ QJsonObject keysJson = toJson(d->oneTimeKeysCount);
+ rootObj.insert(QStringLiteral("device_one_time_keys_count"), keysJson);
+ }
+#endif
- QJsonObject versionObj;
- versionObj.insert("major", CACHE_VERSION_MAJOR);
- versionObj.insert("minor", CACHE_VERSION_MINOR);
- rootObj.insert("cache_version", versionObj);
-
- QJsonDocument json { rootObj };
- auto data = d->cacheToBinary ? json.toBinaryData() :
- json.toJson(QJsonDocument::Compact);
+ const auto data =
+ d->cacheToBinary ? QCborValue::fromJsonValue(rootObj).toCbor()
+ : QJsonDocument(rootObj).toJson(QJsonDocument::Compact);
qCDebug(PROFILER) << "Cache for" << userId() << "generated in" << et;
- outfile.write(data.data(), data.size());
- qCDebug(MAIN) << "State cache saved to" << outfile.fileName();
+ outFile.write(data.data(), data.size());
+ qCDebug(MAIN) << "State cache saved to" << outFile.fileName();
}
-void Connection::loadState(const QUrl &fromFile)
+void Connection::loadState()
{
if (!d->cacheState)
return;
- QElapsedTimer et; et.start();
- QFile file {
- fromFile.isEmpty() ? stateCachePath() : fromFile.toLocalFile()
- };
- if (!file.exists())
- {
- qCDebug(MAIN) << "No state cache file found";
- return;
- }
- if(!file.open(QFile::ReadOnly))
- {
- qCWarning(MAIN) << "file " << file.fileName() << "failed to open for read";
- return;
- }
- QByteArray data = file.readAll();
+ QElapsedTimer et;
+ et.start();
- auto jsonDoc = d->cacheToBinary ? QJsonDocument::fromBinaryData(data) :
- QJsonDocument::fromJson(data);
- if (jsonDoc.isNull())
- {
- qCWarning(MAIN) << "Cache file broken, discarding";
+ SyncData sync { d->topLevelStatePath() };
+ if (sync.nextBatch().isEmpty()) // No token means no cache by definition
return;
- }
- auto actualCacheVersionMajor =
- jsonDoc.object()
- .value("cache_version").toObject()
- .value("major").toInt();
- if (actualCacheVersionMajor < CACHE_VERSION_MAJOR)
- {
- qCWarning(MAIN)
- << "Major version of the cache file is" << actualCacheVersionMajor
- << "but" << CACHE_VERSION_MAJOR << "required; discarding the cache";
+
+ if (!sync.unresolvedRooms().isEmpty()) {
+ qCWarning(MAIN) << "State cache incomplete, discarding";
return;
}
-
- SyncData sync;
- sync.parseJson(jsonDoc);
- onSyncSuccess(std::move(sync));
+ // TODO: to handle load failures, instead of the above block:
+ // 1. Do initial sync on failed rooms without saving the nextBatch token
+ // 2. Do the sync across all rooms as normal
+ onSyncSuccess(std::move(sync), true);
qCDebug(PROFILER) << "*** Cached state for" << userId() << "loaded in" << et;
}
QString Connection::stateCachePath() const
{
- auto safeUserId = userId();
- safeUserId.replace(':', '_');
- return QStandardPaths::writableLocation(QStandardPaths::CacheLocation)
- % '/' % safeUserId % "_state.json";
+ return stateCacheDir().path() % '/';
}
-bool Connection::cacheState() const
+QDir Connection::stateCacheDir() const
{
- return d->cacheState;
+ auto safeUserId = userId();
+ safeUserId.replace(':', '_');
+ return cacheLocation(safeUserId);
}
+bool Connection::cacheState() const { return d->cacheState; }
+
void Connection::setCacheState(bool newValue)
{
- if (d->cacheState != newValue)
- {
+ if (d->cacheState != newValue) {
d->cacheState = newValue;
emit cacheStateChanged();
}
}
+bool Connection::lazyLoading() const { return d->lazyLoading; }
+
+void Connection::setLazyLoading(bool newValue)
+{
+ if (d->lazyLoading != newValue) {
+ d->lazyLoading = newValue;
+ emit lazyLoadingChanged();
+ }
+}
+
+BaseJob* Connection::run(BaseJob* job, RunningPolicy runningPolicy)
+{
+ // Reparent to protect from #397, #398 and to prevent BaseJob* from being
+ // garbage-collected if made by or returned to QML/JavaScript.
+ job->setParent(this);
+ connect(job, &BaseJob::failure, this, &Connection::requestFailed);
+ job->initiate(d->data.get(), runningPolicy & BackgroundRequest);
+ return job;
+}
+
void Connection::getTurnServers()
{
- auto job = callApi<GetTurnServerJob>();
- connect( job, &GetTurnServerJob::success, [=] {
- emit turnServersChanged(job->data());
- });
+ auto job = callApi<GetTurnServerJob>();
+ connect(job, &GetTurnServerJob::success, this,
+ [this,job] { emit turnServersChanged(job->data()); });
+}
+
+const QString Connection::SupportedRoomVersion::StableTag =
+ QStringLiteral("stable");
+
+QString Connection::defaultRoomVersion() const
+{
+ return d->capabilities.roomVersions
+ ? d->capabilities.roomVersions->defaultVersion
+ : QString();
+}
+
+QStringList Connection::stableRoomVersions() const
+{
+ QStringList l;
+ if (d->capabilities.roomVersions) {
+ const auto& allVersions = d->capabilities.roomVersions->available;
+ for (auto it = allVersions.begin(); it != allVersions.end(); ++it)
+ if (it.value() == SupportedRoomVersion::StableTag)
+ l.push_back(it.key());
+ }
+ return l;
+}
+
+bool Connection::canChangePassword() const
+{
+ // By default assume we can
+ return d->capabilities.changePassword
+ ? d->capabilities.changePassword->enabled
+ : true;
+}
+
+inline bool roomVersionLess(const Connection::SupportedRoomVersion& v1,
+ const Connection::SupportedRoomVersion& v2)
+{
+ bool ok1 = false, ok2 = false;
+ const auto vNum1 = v1.id.toFloat(&ok1);
+ const auto vNum2 = v2.id.toFloat(&ok2);
+ return ok1 && ok2 ? vNum1 < vNum2 : v1.id < v2.id;
+}
+
+QVector<Connection::SupportedRoomVersion> Connection::availableRoomVersions() const
+{
+ QVector<SupportedRoomVersion> result;
+ if (d->capabilities.roomVersions) {
+ const auto& allVersions = d->capabilities.roomVersions->available;
+ result.reserve(allVersions.size());
+ for (auto it = allVersions.begin(); it != allVersions.end(); ++it)
+ result.push_back({ it.key(), it.value() });
+ // Put stable versions over unstable; within each group,
+ // sort numeric versions as numbers, the rest as strings.
+ const auto mid =
+ std::partition(result.begin(), result.end(),
+ std::mem_fn(&SupportedRoomVersion::isStable));
+ std::sort(result.begin(), mid, roomVersionLess);
+ std::sort(mid, result.end(), roomVersionLess);
+ }
+ return result;
+}
+
+#ifdef Quotient_E2EE_ENABLED
+void Connection::Private::loadOutdatedUserDevices()
+{
+ QHash<QString, QStringList> users;
+ for(const auto &user : outdatedUsers) {
+ users[user] += QStringList();
+ }
+ if(currentQueryKeysJob) {
+ currentQueryKeysJob->abandon();
+ currentQueryKeysJob = nullptr;
+ }
+ auto queryKeysJob = q->callApi<QueryKeysJob>(users);
+ currentQueryKeysJob = queryKeysJob;
+ connect(queryKeysJob, &BaseJob::success, q, [this, queryKeysJob](){
+ currentQueryKeysJob = nullptr;
+ const auto data = queryKeysJob->deviceKeys();
+ for(const auto &[user, keys] : asKeyValueRange(data)) {
+ QHash<QString, Quotient::DeviceKeys> oldDevices = deviceKeys[user];
+ deviceKeys[user].clear();
+ for(const auto &device : keys) {
+ if(device.userId != user) {
+ qCWarning(E2EE)
+ << "mxId mismatch during device key verification:"
+ << device.userId << user;
+ continue;
+ }
+ if (!std::all_of(device.algorithms.cbegin(),
+ device.algorithms.cend(),
+ isSupportedAlgorithm)) {
+ qCWarning(E2EE) << "Unsupported encryption algorithms found"
+ << device.algorithms;
+ continue;
+ }
+ if (!verifyIdentitySignature(device, device.deviceId,
+ device.userId)) {
+ qCWarning(E2EE) << "Failed to verify devicekeys signature. "
+ "Skipping this device";
+ continue;
+ }
+ if (oldDevices.contains(device.deviceId)) {
+ if (oldDevices[device.deviceId].keys["ed25519:" % device.deviceId] != device.keys["ed25519:" % device.deviceId]) {
+ qCDebug(E2EE) << "Device reuse detected. Skipping this device";
+ continue;
+ }
+ }
+ deviceKeys[user][device.deviceId] = SLICE(device, DeviceKeys);
+ }
+ outdatedUsers -= user;
+ }
+ saveDevicesList();
+
+ for(size_t i = 0; i < pendingEncryptedEvents.size();) {
+ if (isKnownCurveKey(
+ pendingEncryptedEvents[i]->fullJson()[SenderKeyL].toString(),
+ pendingEncryptedEvents[i]->contentPart<QString>("sender_key"_ls))) {
+ handleEncryptedToDeviceEvent(*pendingEncryptedEvents[i]);
+ pendingEncryptedEvents.erase(pendingEncryptedEvents.begin() + i);
+ } else
+ ++i;
+ }
+ });
+}
+
+void Connection::Private::saveDevicesList()
+{
+ q->database()->transaction();
+ auto query = q->database()->prepareQuery(
+ QStringLiteral("DELETE FROM tracked_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral(
+ "INSERT INTO tracked_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : trackedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+ query.prepare(QStringLiteral("DELETE FROM outdated_users"));
+ q->database()->execute(query);
+ query.prepare(QStringLiteral(
+ "INSERT INTO outdated_users(matrixId) VALUES(:matrixId);"));
+ for (const auto& user : outdatedUsers) {
+ query.bindValue(":matrixId", user);
+ q->database()->execute(query);
+ }
+
+ query.prepare(QStringLiteral(
+ "INSERT INTO tracked_devices"
+ "(matrixId, deviceId, curveKeyId, curveKey, edKeyId, edKey, verified) "
+ "SELECT :matrixId, :deviceId, :curveKeyId, :curveKey, :edKeyId, :edKey, :verified WHERE NOT EXISTS(SELECT 1 FROM tracked_devices WHERE matrixId=:matrixId AND deviceId=:deviceId);"
+ ));
+ for (const auto& user : deviceKeys.keys()) {
+ for (const auto& device : deviceKeys[user]) {
+ auto keys = device.keys.keys();
+ auto curveKeyId = keys[0].startsWith(QLatin1String("curve")) ? keys[0] : keys[1];
+ auto edKeyId = keys[0].startsWith(QLatin1String("ed")) ? keys[0] : keys[1];
+
+ query.bindValue(":matrixId", user);
+ query.bindValue(":deviceId", device.deviceId);
+ query.bindValue(":curveKeyId", curveKeyId);
+ query.bindValue(":curveKey", device.keys[curveKeyId]);
+ query.bindValue(":edKeyId", edKeyId);
+ query.bindValue(":edKey", device.keys[edKeyId]);
+ // If the device gets saved here, it can't be verified
+ query.bindValue(":verified", false);
+
+ q->database()->execute(query);
+ }
+ }
+ q->database()->commit();
}
+
+void Connection::Private::loadDevicesList()
+{
+ auto query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ trackedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM outdated_users;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ outdatedUsers += query.value(0).toString();
+ }
+
+ query = q->database()->prepareQuery(QStringLiteral("SELECT * FROM tracked_devices;"));
+ q->database()->execute(query);
+ while(query.next()) {
+ deviceKeys[query.value("matrixId").toString()][query.value("deviceId").toString()] = DeviceKeys {
+ query.value("matrixId").toString(),
+ query.value("deviceId").toString(),
+ { "m.olm.v1.curve25519-aes-sha2", "m.megolm.v1.aes-sha2"},
+ {{query.value("curveKeyId").toString(), query.value("curveKey").toString()},
+ {query.value("edKeyId").toString(), query.value("edKey").toString()}},
+ {} // Signatures are not saved/loaded as they are not needed after initial validation
+ };
+ }
+
+}
+
+void Connection::encryptionUpdate(Room *room)
+{
+ for(const auto &user : room->users()) {
+ if(!d->trackedUsers.contains(user->id())) {
+ d->trackedUsers += user->id();
+ d->outdatedUsers += user->id();
+ d->encryptionUpdateRequired = true;
+ }
+ }
+}
+
+PicklingMode Connection::picklingMode() const
+{
+ return d->picklingMode;
+}
+#endif
+
+void Connection::saveOlmAccount()
+{
+#ifdef Quotient_E2EE_ENABLED
+ qCDebug(E2EE) << "Saving olm account";
+ d->database->setAccountPickle(d->olmAccount->pickle(d->picklingMode));
+#endif
+}
+
+#ifdef Quotient_E2EE_ENABLED
+QJsonObject Connection::decryptNotification(const QJsonObject &notification)
+{
+ if (auto r = room(notification["room_id"].toString()))
+ if (auto event =
+ loadEvent<EncryptedEvent>(notification["event"].toObject()))
+ if (const auto decrypted = r->decryptMessage(*event))
+ return decrypted->fullJson();
+ return QJsonObject();
+}
+
+Database* Connection::database() const
+{
+ return d->database;
+}
+
+UnorderedMap<QString, QOlmInboundGroupSessionPtr>
+Connection::loadRoomMegolmSessions(const Room* room) const
+{
+ return database()->loadMegolmSessions(room->id(), picklingMode());
+}
+
+void Connection::saveMegolmSession(const Room* room,
+ const QOlmInboundGroupSession& session) const
+{
+ database()->saveMegolmSession(room->id(), session.sessionId(),
+ session.pickle(picklingMode()),
+ session.senderId(), session.olmSessionId());
+}
+
+QStringList Connection::devicesForUser(const QString& userId) const
+{
+ return d->deviceKeys[userId].keys();
+}
+
+QString Connection::Private::curveKeyForUserDevice(const QString& userId,
+ const QString& device) const
+{
+ return deviceKeys[userId][device].keys["curve25519:" % device];
+}
+
+QString Connection::edKeyForUserDevice(const QString& userId,
+ const QString& deviceId) const
+{
+ return d->deviceKeys[userId][deviceId].keys["ed25519:" % deviceId];
+}
+
+bool Connection::Private::isKnownCurveKey(const QString& userId,
+ const QString& curveKey) const
+{
+ auto query = database->prepareQuery(
+ QStringLiteral("SELECT * FROM tracked_devices WHERE matrixId=:matrixId "
+ "AND curveKey=:curveKey"));
+ query.bindValue(":matrixId", userId);
+ query.bindValue(":curveKey", curveKey);
+ database->execute(query);
+ return query.next();
+}
+
+bool Connection::hasOlmSession(const QString& user,
+ const QString& deviceId) const
+{
+ const auto& curveKey = d->curveKeyForUserDevice(user, deviceId);
+ return d->olmSessions.contains(curveKey) && !d->olmSessions[curveKey].empty();
+}
+
+std::pair<QOlmMessage::Type, QByteArray> Connection::Private::olmEncryptMessage(
+ const QString& userId, const QString& device,
+ const QByteArray& message) const
+{
+ const auto& curveKey = curveKeyForUserDevice(userId, device);
+ const auto& olmSession = olmSessions.at(curveKey).front();
+ const auto result = olmSession->encrypt(message);
+ database->updateOlmSession(curveKey, olmSession->sessionId(),
+ olmSession->pickle(picklingMode));
+ return { result.type(), result.toCiphertext() };
+}
+
+bool Connection::Private::createOlmSession(const QString& targetUserId,
+ const QString& targetDeviceId,
+ const OneTimeKeys& oneTimeKeyObject)
+{
+ static QOlmUtility verifier;
+ qDebug(E2EE) << "Creating a new session for" << targetUserId
+ << targetDeviceId;
+ if (oneTimeKeyObject.isEmpty()) {
+ qWarning(E2EE) << "No one time key for" << targetUserId
+ << targetDeviceId;
+ return false;
+ }
+ auto* signedOneTimeKey =
+ std::get_if<SignedOneTimeKey>(&*oneTimeKeyObject.begin());
+ if (!signedOneTimeKey) {
+ qWarning(E2EE) << "No signed one time key for" << targetUserId
+ << targetDeviceId;
+ return false;
+ }
+ // Verify contents of signedOneTimeKey - for that, drop `signatures` and
+ // `unsigned` and then verify the object against the respective signature
+ const auto signature =
+ signedOneTimeKey->signature(targetUserId, targetDeviceId);
+ if (!verifier.ed25519Verify(
+ q->edKeyForUserDevice(targetUserId, targetDeviceId).toLatin1(),
+ signedOneTimeKey->toJsonForVerification(),
+ signature)) {
+ qWarning(E2EE) << "Failed to verify one-time-key signature for"
+ << targetUserId << targetDeviceId
+ << ". Skipping this device.";
+ return false;
+ }
+ const auto recipientCurveKey =
+ curveKeyForUserDevice(targetUserId, targetDeviceId).toLatin1();
+ auto session =
+ QOlmSession::createOutboundSession(olmAccount.get(), recipientCurveKey,
+ signedOneTimeKey->key());
+ if (!session) {
+ qCWarning(E2EE) << "Failed to create olm session for "
+ << recipientCurveKey << session.error();
+ return false;
+ }
+ saveSession(**session, recipientCurveKey);
+ olmSessions[recipientCurveKey].push_back(std::move(*session));
+ return true;
+}
+
+QJsonObject Connection::Private::assembleEncryptedContent(
+ QJsonObject payloadJson, const QString& targetUserId,
+ const QString& targetDeviceId) const
+{
+ payloadJson.insert(SenderKeyL, data->userId());
+// eventJson.insert("sender_device"_ls, data->deviceId());
+ payloadJson.insert("keys"_ls,
+ QJsonObject{
+ { Ed25519Key,
+ QString(olmAccount->identityKeys().ed25519) } });
+ payloadJson.insert("recipient"_ls, targetUserId);
+ payloadJson.insert(
+ "recipient_keys"_ls,
+ QJsonObject{ { Ed25519Key,
+ q->edKeyForUserDevice(targetUserId, targetDeviceId) } });
+ const auto [type, cipherText] = olmEncryptMessage(
+ targetUserId, targetDeviceId,
+ QJsonDocument(payloadJson).toJson(QJsonDocument::Compact));
+ QJsonObject encrypted {
+ { curveKeyForUserDevice(targetUserId, targetDeviceId),
+ QJsonObject { { "type"_ls, type },
+ { "body"_ls, QString(cipherText) } } }
+ };
+ return EncryptedEvent(encrypted, olmAccount->identityKeys().curve25519)
+ .contentJson();
+}
+
+void Connection::sendSessionKeyToDevices(
+ const QString& roomId, const QByteArray& sessionId,
+ const QByteArray& sessionKey, const QMultiHash<QString, QString>& devices,
+ int index)
+{
+ qDebug(E2EE) << "Sending room key to devices:" << sessionId
+ << sessionKey.toHex();
+ QHash<QString, QHash<QString, QString>> hash;
+ for (const auto& [userId, deviceId] : asKeyValueRange(devices))
+ if (!hasOlmSession(userId, deviceId)) {
+ hash[userId].insert(deviceId, "signed_curve25519"_ls);
+ qDebug(E2EE) << "Adding" << userId << deviceId
+ << "to keys to claim";
+ }
+
+ if (hash.isEmpty())
+ return;
+
+ auto job = callApi<ClaimKeysJob>(hash);
+ connect(job, &BaseJob::success, this, [job, this, roomId, sessionId, sessionKey, devices, index] {
+ QHash<QString, QHash<QString, QJsonObject>> usersToDevicesToContent;
+ for (const auto oneTimeKeys = job->oneTimeKeys();
+ const auto& [targetUserId, targetDeviceId] :
+ asKeyValueRange(devices)) {
+ if (!hasOlmSession(targetUserId, targetDeviceId)
+ && !d->createOlmSession(
+ targetUserId, targetDeviceId,
+ oneTimeKeys[targetUserId][targetDeviceId]))
+ continue;
+
+ // Noisy but nice for debugging
+// qDebug(E2EE) << "Creating the payload for" << targetUserId
+// << targetDeviceId << sessionId << sessionKey.toHex();
+ const auto keyEventJson = RoomKeyEvent(MegolmV1AesSha2AlgoKey,
+ roomId, sessionId, sessionKey)
+ .fullJson();
+
+ usersToDevicesToContent[targetUserId][targetDeviceId] =
+ d->assembleEncryptedContent(keyEventJson, targetUserId,
+ targetDeviceId);
+ }
+ if (!usersToDevicesToContent.empty()) {
+ sendToDevices(EncryptedEvent::TypeId, usersToDevicesToContent);
+ QVector<std::tuple<QString, QString, QString>> receivedDevices;
+ receivedDevices.reserve(devices.size());
+ for (const auto& [user, device] : asKeyValueRange(devices))
+ receivedDevices.push_back(
+ { user, device, d->curveKeyForUserDevice(user, device) });
+
+ database()->setDevicesReceivedKey(roomId, receivedDevices,
+ sessionId, index);
+ }
+ });
+}
+
+QOlmOutboundGroupSessionPtr Connection::loadCurrentOutboundMegolmSession(
+ const QString& roomId) const
+{
+ return d->database->loadCurrentOutboundMegolmSession(roomId,
+ d->picklingMode);
+}
+
+void Connection::saveCurrentOutboundMegolmSession(
+ const QString& roomId, const QOlmOutboundGroupSession& session) const
+{
+ d->database->saveCurrentOutboundMegolmSession(roomId, d->picklingMode,
+ session);
+}
+
+void Connection::startKeyVerificationSession(const QString& deviceId)
+{
+ auto* const session = new KeyVerificationSession(userId(), deviceId, this);
+ emit newKeyVerificationSession(session);
+}
+
+void Connection::sendToDevice(const QString& targetUserId,
+ const QString& targetDeviceId, const Event& event,
+ bool encrypted)
+{
+ const auto contentJson =
+ encrypted ? d->assembleEncryptedContent(event.fullJson(), targetUserId,
+ targetDeviceId)
+ : event.contentJson();
+ sendToDevices(encrypted ? EncryptedEvent::TypeId : event.type(),
+ { { targetUserId, { { targetDeviceId, contentJson } } } });
+}
+
+bool Connection::isVerifiedSession(const QString& megolmSessionId) const
+{
+ auto query = database()->prepareQuery("SELECT olmSessionId FROM inbound_megolm_sessions WHERE sessionId=:sessionId;"_ls);
+ query.bindValue(":sessionId", megolmSessionId);
+ database()->execute(query);
+ if (!query.next()) {
+ return false;
+ }
+ auto olmSessionId = query.value("olmSessionId").toString();
+ query.prepare("SELECT senderKey FROM olm_sessions WHERE sessionId=:sessionId;"_ls);
+ query.bindValue(":sessionId", olmSessionId);
+ database()->execute(query);
+ if (!query.next()) {
+ return false;
+ }
+ auto curveKey = query.value("senderKey"_ls).toString();
+ query.prepare("SELECT verified FROM tracked_devices WHERE curveKey=:curveKey;"_ls);
+ query.bindValue(":curveKey", curveKey);
+ database()->execute(query);
+ return query.next() && query.value("verified").toBool();
+}
+#endif