diff options
-rw-r--r-- | lib/connection.cpp | 114 | ||||
-rw-r--r-- | lib/connection.h | 66 | ||||
-rw-r--r-- | lib/events/event.cpp | 3 | ||||
-rw-r--r-- | lib/events/event.h | 41 | ||||
-rw-r--r-- | lib/jobs/basejob.cpp | 6 | ||||
-rw-r--r-- | lib/jobs/basejob.h | 2 | ||||
-rw-r--r-- | lib/jobs/postreadmarkersjob.h | 38 | ||||
-rw-r--r-- | lib/logging.cpp | 1 | ||||
-rw-r--r-- | lib/logging.h | 1 | ||||
-rw-r--r-- | lib/room.cpp | 256 | ||||
-rw-r--r-- | lib/util.h | 2 | ||||
-rw-r--r-- | libquotient.pri | 1 | ||||
-rw-r--r-- | tests/quotest.cpp | 8 |
13 files changed, 302 insertions, 237 deletions
diff --git a/lib/connection.cpp b/lib/connection.cpp index b76ca691..42b17570 100644 --- a/lib/connection.cpp +++ b/lib/connection.cpp @@ -37,6 +37,7 @@ #include "csapi/versions.h" #include "csapi/voip.h" #include "csapi/wellknown.h" +#include "csapi/whoami.h" #include "events/directchatevent.h" #include "events/eventloader.h" @@ -133,10 +134,27 @@ public: != "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 assumeIdentity(const QString& userId, const QString& accessToken, - const QString& deviceId); + void completeSetup(const QString &mxId); void removeRoom(const QString& roomId); void consumeRoomData(SyncDataList&& roomDataList, bool fromCache); @@ -264,12 +282,15 @@ void Connection::resolveServer(const QString& mxid) return; } - auto domain = maybeBaseUrl.host(); - qCDebug(MAIN) << "Finding the server" << domain; + qCDebug(MAIN) << "Finding the server" << maybeBaseUrl.host(); - d->data->setBaseUrl(maybeBaseUrl); // Just enough to check .well-known file + const auto& oldBaseUrl = d->data->baseUrl(); + d->data->setBaseUrl(maybeBaseUrl); // Temporarily set it for this one call d->resolverJob = callApi<GetWellknownJob>(); - connect(d->resolverJob, &BaseJob::finished, this, [this, maybeBaseUrl] { + 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::NotFoundError) { if (!d->resolverJob->status().good()) { qCWarning(MAIN) @@ -297,6 +318,7 @@ void Connection::resolveServer(const QString& mxid) << "for base URL"; setHomeserver(maybeBaseUrl); } + Q_ASSERT(d->loginFlowsJob != nullptr); connect(d->loginFlowsJob, &BaseJob::success, this, &Connection::resolved); connect(d->loginFlowsJob, &BaseJob::failure, this, [this] { @@ -324,10 +346,10 @@ void Connection::loginWithPassword(const QString& userId, const QString& initialDeviceName, const QString& deviceId) { - checkAndConnect(userId, [=] { + d->checkAndConnect(userId, [=] { d->loginToServer(LoginFlows::Password.type, makeUserIdentifier(userId), password, /*token*/ "", deviceId, initialDeviceName); - }); + }, LoginFlows::Password); } SsoSession* Connection::prepareForSso(const QString& initialDeviceName, @@ -340,17 +362,30 @@ 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& userId, - const QString& accessToken, +void Connection::assumeIdentity(const QString& mxId, const QString& accessToken, const QString& deviceId) { - checkAndConnect(userId, - [=] { d->assumeIdentity(userId, accessToken, 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(job, &BaseJob::failure, this, [this, job] { + emit loginError(job->errorString(), job->rawDataSample()); + }); + }); } void Connection::reloadCapabilities() @@ -390,8 +425,9 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) auto loginJob = q->callApi<LoginJob>(std::forward<LoginArgTs>(loginArgs)...); connect(loginJob, &BaseJob::success, q, [this, loginJob] { - assumeIdentity(loginJob->userId(), loginJob->accessToken(), - loginJob->deviceId()); + data->setToken(loginJob->accessToken().toLatin1()); + data->setDeviceId(loginJob->deviceId()); + completeSetup(loginJob->userId()); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED @@ -404,21 +440,18 @@ void Connection::Private::loginToServer(LoginArgTs&&... loginArgs) }); } -void Connection::Private::assumeIdentity(const QString& userId, - const QString& accessToken, - const QString& deviceId) +void Connection::Private::completeSetup(const QString& mxId) { - data->setUserId(userId); + data->setUserId(mxId); q->user(); // Creates a User object for the local user - data->setToken(accessToken.toLatin1()); - data->setDeviceId(deviceId); - q->setObjectName(userId % '/' % 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(); #ifndef Quotient_E2EE_ENABLED qCWarning(E2EE) << "End-to-end encryption (E2EE) support is turned off."; #else // Quotient_E2EE_ENABLED - AccountSettings accountSettings(userId); + AccountSettings accountSettings(data->userId()); encryptionManager.reset( new EncryptionManager(accountSettings.encryptionAccountPickle())); if (accountSettings.encryptionAccountPickle().isEmpty()) { @@ -431,22 +464,37 @@ void Connection::Private::assumeIdentity(const QString& userId, 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. + // Not good to go, try to ascertain the homeserver URL and flows 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); + q->resolveServer(userId); + if (flow) + connectSingleShot(q, &Connection::loginFlowsChanged, q, + [this, flow, connectFn] { + if (loginFlows.contains(*flow)) + connectFn(); + else + emit q->loginError( + 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() diff --git a/lib/connection.h b/lib/connection.h index 07ae9f29..dbe179e8 100644 --- a/lib/connection.h +++ b/lib/connection.h @@ -62,28 +62,27 @@ class SendToDeviceJob; class SendMessageJob; class LeaveRoomJob; +using LoginFlow = GetLoginFlowsJob::LoginFlow; + +/// Predefined login flows +struct LoginFlows { + static inline const LoginFlow Password { "m.login.password" }; + static inline const LoginFlow SSO { "m.login.sso" }; + static inline const LoginFlow Token { "m.login.token" }; +}; + // To simplify comparisons of LoginFlows -inline bool operator==(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) +inline bool operator==(const LoginFlow& lhs, const LoginFlow& rhs) { return lhs.type == rhs.type; } -inline bool operator!=(const GetLoginFlowsJob::LoginFlow& lhs, - const GetLoginFlowsJob::LoginFlow& rhs) +inline bool operator!=(const LoginFlow& lhs, const LoginFlow& rhs) { return !(lhs == rhs); } -/// Predefined login flows -struct LoginFlows { - using LoginFlow = GetLoginFlowsJob::LoginFlow; - static inline const LoginFlow Password { "m.login.password" }; - static inline const LoginFlow SSO { "m.login.sso" }; - static inline const LoginFlow Token { "m.login.token" }; -}; - class Connection; using room_factory_t = @@ -504,13 +503,35 @@ public Q_SLOTS: /** Determine and set the homeserver from MXID */ void resolveServer(const QString& mxid); + /** \brief Log in using a username and password pair + * + * Before logging in, this method checks if the homeserver is valid and + * supports the password login flow. If the homeserver is invalid but + * a full user MXID is provided, this method calls resolveServer() using + * this MXID. + * + * \sa resolveServer, resolveError, loginError + */ void loginWithPassword(const QString& userId, const QString& password, const QString& initialDeviceName, const QString& deviceId = {}); + /** \brief Log in using a login token + * + * One usual case for this method is the final stage of logging in via SSO. + * Unlike loginWithPassword() and assumeIdentity(), this method cannot + * resolve the server from the user name because the full user MXID is + * encoded in the login token. Callers should ensure the homeserver + * sanity in advance. + */ void loginWithToken(const QByteArray& loginToken, const QString& initialDeviceName, const QString& deviceId = {}); - void assumeIdentity(const QString& userId, const QString& accessToken, + /** \brief Use an existing access token to connect to the homeserver + * + * Similar to loginWithPassword(), this method checks that the homeserver + * URL is valid and tries to resolve it from the MXID in case it is not. + */ + void assumeIdentity(const QString& mxId, const QString& accessToken, const QString& deviceId); /*! \deprecated Use loginWithPassword instead */ void connectToServer(const QString& userId, const QString& password, @@ -662,9 +683,9 @@ Q_SIGNALS: * This was a signal resulting from a successful resolveServer(). * Since Connection now provides setHomeserver(), the HS URL * may change even without resolveServer() invocation. Use - * homeserverChanged() instead of resolved(). You can also use - * connectToServer and connectWithToken without the HS URL set in - * advance (i.e. without calling resolveServer), as they now trigger + * loginFLowsChanged() instead of resolved(). You can also use + * loginWith*() and assumeIdentity() without the HS URL set in + * advance (i.e. without calling resolveServer), as they trigger * server name resolution from MXID if the server URL is not valid. */ void resolved(); @@ -860,19 +881,6 @@ private: class Private; QScopedPointer<Private> d; - /** - * A single entry for functions that need to check whether the - * homeserver is valid before running. May either execute connectFn - * synchronously or asynchronously (if tryResolve is true and - * a DNS lookup is initiated); in case of errors, emits resolveError - * if the homeserver URL is not valid and cannot be resolved from - * userId. - * - * @param userId - fully-qualified MXID to resolve HS from - * @param connectFn - a function to execute once the HS URL is good - */ - void checkAndConnect(const QString& userId, std::function<void()> connectFn); - static room_factory_t _roomFactory; static user_factory_t _userFactory; }; diff --git a/lib/events/event.cpp b/lib/events/event.cpp index 96e33864..7b34114d 100644 --- a/lib/events/event.cpp +++ b/lib/events/event.cpp @@ -61,11 +61,14 @@ QString Event::matrixType() const { return fullJson()[TypeKeyL].toString(); } QByteArray Event::originalJson() const { return QJsonDocument(_json).toJson(); } +// On const below: this is to catch accidental attempts to change event JSON +// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::contentJson() const { return fullJson()[ContentKeyL].toObject(); } +// NOLINTNEXTLINE(readability-const-return-type) const QJsonObject Event::unsignedJson() const { return fullJson()[UnsignedKeyL].toObject(); diff --git a/lib/events/event.h b/lib/events/event.h index 626a0229..9f2f4f91 100644 --- a/lib/events/event.h +++ b/lib/events/event.h @@ -298,7 +298,7 @@ using Events = EventsArray<Event>; // === is<>(), eventCast<>() and visit<>() === -template <typename EventT> +template <class EventT> inline bool is(const Event& e) { return e.type() == typeId<EventT>(); @@ -309,7 +309,7 @@ inline bool isUnknown(const Event& e) return e.type() == unknownEventTypeId(); } -template <typename EventT, typename BasePtrT> +template <class EventT, typename BasePtrT> inline auto eventCast(const BasePtrT& eptr) -> decltype(static_cast<EventT*>(&*eptr)) { @@ -319,7 +319,7 @@ inline auto eventCast(const BasePtrT& eptr) } // A single generic catch-all visitor -template <typename BaseEventT, typename FnT> +template <class BaseEventT, typename FnT> inline auto visit(const BaseEventT& event, FnT&& visitor) -> decltype(visitor(event)) { @@ -327,18 +327,18 @@ inline auto visit(const BaseEventT& event, FnT&& visitor) } namespace _impl { - template <typename T, typename FnT> - constexpr auto needs_downcast() - { - return !std::is_convertible_v<T, fn_arg_t<FnT>>; - } + // Using bool instead of auto below because auto apparently upsets MSVC + template <class BaseT, typename FnT> + inline constexpr bool needs_downcast = + std::is_base_of_v<BaseT, std::decay_t<fn_arg_t<FnT>>> + && !std::is_same_v<BaseT, std::decay_t<fn_arg_t<FnT>>>; } // A single type-specific void visitor -template <typename BaseEventT, typename FnT> -inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>() +template <class BaseT, typename FnT> +inline auto visit(const BaseT& event, FnT&& visitor) + -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT> && std::is_void_v<fn_return_t<FnT>>> -visit(const BaseEventT& event, FnT&& visitor) { using event_type = fn_arg_t<FnT>; if (is<std::decay_t<event_type>>(event)) @@ -347,10 +347,10 @@ visit(const BaseEventT& event, FnT&& visitor) // A single type-specific non-void visitor with an optional default value // non-voidness is guarded by defaultValue type -template <typename BaseEventT, typename FnT> -inline std::enable_if_t<_impl::needs_downcast<BaseEventT, FnT>(), fn_return_t<FnT>> -visit(const BaseEventT& event, FnT&& visitor, - fn_return_t<FnT>&& defaultValue = {}) +template <class BaseT, typename FnT> +inline auto visit(const BaseT& event, FnT&& visitor, + fn_return_t<FnT>&& defaultValue = {}) + -> std::enable_if_t<_impl::needs_downcast<BaseT, FnT>, fn_return_t<FnT>> { using event_type = fn_arg_t<FnT>; if (is<std::decay_t<event_type>>(event)) @@ -359,9 +359,10 @@ visit(const BaseEventT& event, FnT&& visitor, } // A chain of 2 or more visitors -template <typename BaseEventT, typename FnT1, typename FnT2, typename... FnTs> -inline fn_return_t<FnT1> visit(const BaseEventT& event, FnT1&& visitor1, - FnT2&& visitor2, FnTs&&... visitors) +template <class BaseT, typename FnT1, typename FnT2, typename... FnTs> +inline std::common_type_t<fn_return_t<FnT1>, fn_return_t<FnT2>> visit( + const BaseT& event, FnT1&& visitor1, FnT2&& visitor2, + FnTs&&... visitors) { using event_type1 = fn_arg_t<FnT1>; if (is<std::decay_t<event_type1>>(event)) @@ -374,8 +375,8 @@ inline fn_return_t<FnT1> visit(const BaseEventT& event, FnT1&& visitor1, // over a range of event pointers template <typename RangeT, typename... FnTs> inline auto visitEach(RangeT&& events, FnTs&&... visitors) - -> std::enable_if_t<std::is_convertible_v< - std::decay_t<decltype(**events.begin())>, Event>> + -> std::enable_if_t<std::is_void_v< + decltype(visit(**begin(events), std::forward<FnTs>(visitors)...))>> { for (auto&& evtPtr: events) visit(*evtPtr, std::forward<FnTs>(visitors)...); diff --git a/lib/jobs/basejob.cpp b/lib/jobs/basejob.cpp index 2ac942f5..d37f05bc 100644 --- a/lib/jobs/basejob.cpp +++ b/lib/jobs/basejob.cpp @@ -263,7 +263,7 @@ void BaseJob::setExpectedContentTypes(const QByteArrayList& contentTypes) d->expectedContentTypes = contentTypes; } -const QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } +QByteArrayList BaseJob::expectedKeys() const { return d->expectedKeys; } void BaseJob::addExpectedKey(const QByteArray& key) { d->expectedKeys << key; } @@ -342,7 +342,7 @@ void BaseJob::beforeAbandon() { } void BaseJob::initiate(ConnectionData* connData, bool inBackground) { - if (connData && connData->baseUrl().isValid()) { + if (Q_LIKELY(connData && connData->baseUrl().isValid())) { d->inBackground = inBackground; d->connection = connData; doPrepare(); @@ -355,7 +355,7 @@ void BaseJob::initiate(ConnectionData* connData, bool inBackground) setStatus(FileError, "Request data not ready"); } Q_ASSERT(status().code != Pending); // doPrepare() must NOT set this - if (status().code == Unprepared) { + if (Q_LIKELY(status().code == Unprepared)) { d->connection->submit(this); return; } diff --git a/lib/jobs/basejob.h b/lib/jobs/basejob.h index a0b89ef7..a72f6120 100644 --- a/lib/jobs/basejob.h +++ b/lib/jobs/basejob.h @@ -363,7 +363,7 @@ protected: const QByteArrayList& expectedContentTypes() const; void addExpectedContentType(const QByteArray& contentType); void setExpectedContentTypes(const QByteArrayList& contentTypes); - const QByteArrayList expectedKeys() const; + QByteArrayList expectedKeys() const; void addExpectedKey(const QByteArray &key); void setExpectedKeys(const QByteArrayList &keys); diff --git a/lib/jobs/postreadmarkersjob.h b/lib/jobs/postreadmarkersjob.h deleted file mode 100644 index 5a4d942c..00000000 --- a/lib/jobs/postreadmarkersjob.h +++ /dev/null @@ -1,38 +0,0 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ - -#pragma once - -#include "basejob.h" - -#include <QtCore/QJsonObject> - -using namespace Quotient; - -class PostReadMarkersJob : public BaseJob { -public: - explicit PostReadMarkersJob(const QString& roomId, - const QString& readUpToEventId) - : BaseJob( - HttpVerb::Post, "PostReadMarkersJob", - QStringLiteral("_matrix/client/r0/rooms/%1/read_markers").arg(roomId)) - { - setRequestData( - QJsonObject { { QStringLiteral("m.fully_read"), readUpToEventId } }); - } -}; diff --git a/lib/logging.cpp b/lib/logging.cpp index c346fbf1..e6f8aa4b 100644 --- a/lib/logging.cpp +++ b/lib/logging.cpp @@ -24,6 +24,7 @@ LOGGING_CATEGORY(MAIN, "quotient.main") LOGGING_CATEGORY(EVENTS, "quotient.events") LOGGING_CATEGORY(STATE, "quotient.events.state") +LOGGING_CATEGORY(MEMBERS, "quotient.events.members") LOGGING_CATEGORY(MESSAGES, "quotient.events.messages") LOGGING_CATEGORY(EPHEMERAL, "quotient.events.ephemeral") LOGGING_CATEGORY(E2EE, "quotient.e2ee") diff --git a/lib/logging.h b/lib/logging.h index ce4131bb..05ca7988 100644 --- a/lib/logging.h +++ b/lib/logging.h @@ -23,6 +23,7 @@ Q_DECLARE_LOGGING_CATEGORY(MAIN) Q_DECLARE_LOGGING_CATEGORY(STATE) +Q_DECLARE_LOGGING_CATEGORY(MEMBERS) Q_DECLARE_LOGGING_CATEGORY(MESSAGES) Q_DECLARE_LOGGING_CATEGORY(EVENTS) Q_DECLARE_LOGGING_CATEGORY(EPHEMERAL) diff --git a/lib/room.cpp b/lib/room.cpp index a19624d8..a9b2ba30 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -36,6 +36,7 @@ #include "csapi/room_state.h" #include "csapi/room_upgrades.h" #include "csapi/rooms.h" +#include "csapi/read_markers.h" #include "csapi/tags.h" #include "events/callanswerevent.h" @@ -55,7 +56,6 @@ #include "events/roompowerlevelsevent.h" #include "jobs/downloadfilejob.h" #include "jobs/mediathumbnailjob.h" -#include "jobs/postreadmarkersjob.h" #include "events/roomcanonicalaliasevent.h" #include <QtCore/QDir> @@ -263,10 +263,11 @@ public: for (auto&& eptr : events) { const auto& evt = *eptr; Q_ASSERT(evt.isStateEvent()); - // Update baseState afterwards to make sure that the old state - // is valid and usable inside processStateEvent - changes |= q->processStateEvent(evt); - baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); + auto change = q->processStateEvent(evt); + if (change != NoChange) { + changes |= change; + baseState[{ evt.matrixType(), evt.stateKey() }] = move(eptr); + } } if (events.size() > 9 || et.nsecsElapsed() >= profilerMinNsecs()) qCDebug(PROFILER) @@ -632,8 +633,8 @@ Room::Changes Room::Private::setLastReadEvent(User* u, QString eventId) emit q->readMarkerForUserMoved(u, eventId, storedId); if (isLocalUser(u)) { if (storedId != serverReadMarker) - connection->callApi<PostReadMarkersJob>(BackgroundRequest, id, - storedId); + connection->callApi<SetReadMarkerJob>(BackgroundRequest, id, + storedId); emit q->readMarkerMoved(eventId, storedId); return Change::ReadMarkerChange; } @@ -1353,23 +1354,27 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) void Room::Private::insertMemberIntoMap(User* u) { - const auto userName = - getCurrentState<RoomMemberEvent>(u->id())->displayName(); - qDebug(STATE) << "insertMemberIntoMap(), user" << u->id() << "with name" - << userName; - // If there is exactly one namesake of the added user, signal member - // renaming for that other one because the two should be disambiguated now. + const auto maybeUserName = + getCurrentState<RoomMemberEvent>(u->id())->newDisplayName(); + if (!maybeUserName) + qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id() + << "has no name (even empty)"; + const auto userName = maybeUserName.value_or(QString()); const auto namesakes = membersMap.values(userName); - qDebug(STATE) << namesakes.size() << "namesake(s) found"; + qCDebug(MEMBERS) << "insertMemberIntoMap(), user" << u->id() + << "with name" << userName << '-' + << namesakes.size() << "namesake(s) found"; - // Callers should check they are not adding an existing user once more. + // Callers should make sure they are not adding an existing user once more Q_ASSERT(!namesakes.contains(u)); if (namesakes.contains(u)) { // Release version whines but continues - qCCritical(STATE) << "Trying to add a user" << u->id() << "to room" - << q->objectName() << "but that's already in it"; + qCCritical(MEMBERS) << "Trying to add a user" << u->id() << "to room" + << q->objectName() << "but that's already in it"; return; } + // If there is exactly one namesake of the added user, signal member + // renaming for that other one because the two should be disambiguated now if (namesakes.size() == 1) emit q->memberAboutToRename(namesakes.front(), namesakes.front()->fullName(q)); @@ -1381,21 +1386,40 @@ void Room::Private::insertMemberIntoMap(User* u) void Room::Private::removeMemberFromMap(User* u) { const auto userName = - getCurrentState<RoomMemberEvent>(u->id())->displayName(); + getCurrentState<RoomMemberEvent>( + u->id())->newDisplayName().value_or(QString()); - qDebug(STATE) << "removeMemberFromMap(), username" << userName << "for user" - << u->id(); + qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName + << "for user" << u->id(); User* namesake = nullptr; auto namesakes = membersMap.values(userName); + // If there was one namesake besides the removed user, signal member + // renaming for it because it doesn't need to be disambiguated any more. if (namesakes.size() == 2) { - namesake = namesakes.front() == u ? namesakes.back() : namesakes.front(); + namesake = + namesakes.front() == u ? namesakes.back() : namesakes.front(); Q_ASSERT_X(namesake != u, __FUNCTION__, "Room members list is broken"); emit q->memberAboutToRename(namesake, userName); } - const auto removed = membersMap.remove(userName, u); - qDebug(STATE) << "Removed" << removed << "entries"; - // If there was one namesake besides the removed user, signal member - // renaming for it because it doesn't need to be disambiguated any more. + if (membersMap.remove(userName, u) == 0) { + qCDebug(MEMBERS) << "No entries removed; checking the whole list"; + // Unless at the stage of initial filling, this no removed entries + // is suspicious; double-check that this user is not found in + // the whole map, and stop (for debug builds) or shout in the logs + // (for release builds) if there's one. That search is O(n), which + // may come rather expensive for larger rooms. + QElapsedTimer et; + auto it = std::find(membersMap.cbegin(), membersMap.cend(), u); + if (et.nsecsElapsed() > profilerMinNsecs() / 10) + qCDebug(MEMBERS) << "...done in" << et; + if (it != membersMap.cend()) { + Q_ASSERT_X(false, __FUNCTION__, + "Mismatched name in the room members list"); + qCCritical(MEMBERS) << "Mismatched name in the room members list;" + " avoiding the list corruption"; + membersMap.remove(it.key(), u); + } + } if (namesake) emit q->memberRenamed(namesake); } @@ -2449,7 +2473,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) Room::Changes Room::processStateEvent(const RoomEvent& e) { if (!e.isStateEvent()) - return Change::NoChange; + return NoChange; // Find a value (create an empty one if necessary) and get a reference // to it. Can't use getCurrentState<>() because it (creates and) returns @@ -2457,48 +2481,87 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) // or nullptr. auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change - visit(e, [this, oldRme = static_cast<const RoomMemberEvent*>(curStateEvent)]( - const RoomMemberEvent& rme) { - auto* u = user(rme.userId()); - if (!u) { // ??? - qCCritical(MAIN) - << "Could not get a user object for" << rme.userId(); - return; - } - const auto prevMembership = oldRme ? oldRme->membership() - : MembershipType::Leave; - switch (prevMembership) { - case MembershipType::Invite: - if (rme.membership() != prevMembership) { - d->usersInvited.removeOne(u); - Q_ASSERT(!d->usersInvited.contains(u)); + // clang-format off + const bool proceed = visit(e + , [this, curStateEvent](const RoomMemberEvent& rme) { + // clang-format on + auto* oldRme = static_cast<const RoomMemberEvent*>(curStateEvent); + auto* u = user(rme.userId()); + if (!u) { // Some terribly malformed user id? + qCCritical(MAIN) << "Could not get a user object for" + << rme.userId(); + return false; // Stay low and hope for the best... } - break; - case MembershipType::Join: - switch (rme.membership()) { - case MembershipType::Join: // rename/avatar change or no-op - if (rme.newDisplayName()) { - emit memberAboutToRename(u, *rme.newDisplayName()); + const auto prevMembership = oldRme ? oldRme->membership() + : MembershipType::Leave; + switch (prevMembership) { + case MembershipType::Invite: + if (rme.membership() != prevMembership) { + d->usersInvited.removeOne(u); + Q_ASSERT(!d->usersInvited.contains(u)); + } + break; + case MembershipType::Join: + if (rme.membership() == MembershipType::Join) { + // rename/avatar change or no-op + if (rme.newDisplayName()) { + emit memberAboutToRename(u, *rme.newDisplayName()); + d->removeMemberFromMap(u); + } + if (!rme.newDisplayName() && !rme.newAvatarUrl()) { + qCWarning(MEMBERS) + << "No-op membership event for" << rme.userId() + << "- retaining the state"; + qCWarning(MEMBERS) << "The event dump:" << rme; + return false; + } + } else { + if (rme.membership() == MembershipType::Invite) + qCWarning(MAIN) + << "Membership change from Join to Invite:" << rme; + // whatever the new membership, it's no more Join d->removeMemberFromMap(u); + emit userRemoved(u); } break; - case MembershipType::Invite: - qCWarning(MAIN) << "Membership change from Join to Invite:" - << rme; - [[fallthrough]]; - default: // whatever the new membership, it's no more Join - d->removeMemberFromMap(u); - emit userRemoved(u); + case MembershipType::Ban: + case MembershipType::Knock: + case MembershipType::Leave: + if (rme.membership() == MembershipType::Invite + || rme.membership() == MembershipType::Join) { + d->membersLeft.removeOne(u); + Q_ASSERT(!d->membersLeft.contains(u)); + } + break; + case MembershipType::Undefined: + ; // A warning will be dropped in the post-processing block below } - break; - default: - if (rme.membership() == MembershipType::Invite - || rme.membership() == MembershipType::Join) { - d->membersLeft.removeOne(u); - Q_ASSERT(!d->membersLeft.contains(u)); + return true; + // clang-format off + } + , [this, curStateEvent]( const EncryptionEvent& ee) { + // clang-format on + auto* oldEncEvt = + static_cast<const EncryptionEvent*>(curStateEvent); + if (ee.algorithm().isEmpty()) { + qWarning(STATE) + << "The encryption event for room" << objectName() + << "doesn't have 'algorithm' specified - ignoring"; + return false; + } + if (oldEncEvt + && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { + qCWarning(STATE) << "The room is already encrypted but a new" + " room encryption event arrived - ignoring"; + return false; } + return true; + // clang-format off } - }); + , true); // By default, go forward with the state change + // clang-format on + if (!proceed) + return NoChange; // Change the state const auto* const oldStateEvent = @@ -2506,47 +2569,35 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) Q_ASSERT(!oldStateEvent || (oldStateEvent->matrixType() == e.matrixType() && oldStateEvent->stateKey() == e.stateKey())); - if (!is<RoomMemberEvent>(e)) // Room member events are too numerous + if (is<RoomMemberEvent>(e)) + qCDebug(MEMBERS) << "Updated room member state:" << e; + else qCDebug(STATE) << "Updated room state:" << e; // Update internal structures as per the change and work out the return value // clang-format off - return visit(e + const auto result = visit(e , [] (const RoomNameEvent&) { return NameChange; } - , [] (const RoomAliasesEvent&) { - return NoChange; // This event has been removed by MSC2432 - } , [this, oldStateEvent] (const RoomCanonicalAliasEvent& cae) { // clang-format on setObjectName(cae.alias().isEmpty() ? d->id : cae.alias()); - QString previousCanonicalAlias = - oldStateEvent - ? static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent) - ->alias() - : QString(); - - auto previousAltAliases = - oldStateEvent - ? static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent) - ->altAliases() - : QStringList(); - - if (!previousCanonicalAlias.isEmpty()) { - previousAltAliases.push_back(previousCanonicalAlias); + const auto* oldCae = + static_cast<const RoomCanonicalAliasEvent*>(oldStateEvent); + QStringList previousAltAliases {}; + if (oldCae) { + previousAltAliases = oldCae->altAliases(); + if (!oldCae->alias().isEmpty()) + previousAltAliases.push_back(oldCae->alias()); } - const auto previousAliases = std::move(previousAltAliases); - auto newAliases = cae.altAliases(); - - if (!cae.alias().isEmpty()) { + if (!cae.alias().isEmpty()) newAliases.push_front(cae.alias()); - } - connection()->updateRoomAliases(id(), previousAliases, newAliases); + connection()->updateRoomAliases(id(), previousAltAliases, newAliases); return AliasesChange; // clang-format off } @@ -2569,13 +2620,11 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) switch (evt.membership()) { case MembershipType::Join: if (prevMembership != MembershipType::Join) { - qDebug(STATE) << "!Join -> Join"; d->insertMemberIntoMap(u); emit userAdded(u); } else { if (evt.newDisplayName()) { - qDebug(STATE) << "After renaming"; - d->insertMemberIntoMap(u); + d->insertMemberIntoMap(u); emit memberRenamed(u); } if (evt.newAvatarUrl()) @@ -2588,33 +2637,23 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) if (u == localUser() && evt.isDirect()) connection()->addToDirectChats(this, user(evt.senderId())); break; - default: + case MembershipType::Knock: + case MembershipType::Ban: + case MembershipType::Leave: if (!d->membersLeft.contains(u)) d->membersLeft.append(u); + break; + case MembershipType::Undefined: + qCWarning(MEMBERS) << "Ignored undefined membership type"; } return MembersChange; // clang-format off } - , [this, oldEncEvt = static_cast<const EncryptionEvent*>(oldStateEvent)]( - const EncryptionEvent& ee) { - // clang-format on - if (ee.algorithm().isEmpty()) { - qWarning(STATE) - << "The encryption event for room" << objectName() - << "doesn't have 'algorithm' specified - ignoring"; - return NoChange; - } - if (oldEncEvt - && oldEncEvt->encryption() != EncryptionEventContent::Undefined) { - qCWarning(STATE) << "The room is already encrypted but a new" - " room encryption event arrived - ignoring"; - return NoChange; - } + , [this] (const EncryptionEvent&) { // As encryption can only be switched on once, emit the signal here // instead of aggregating and emitting in updateData() emit encryption(); return OtherChange; - // clang-format off } , [this] (const RoomTombstoneEvent& evt) { const auto successorId = evt.successorRoomId(); @@ -2631,9 +2670,12 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) }); return OtherChange; + // clang-format off } - ); + , OtherChange); // clang-format on + Q_ASSERT(result != NoChange); + return result; } Room::Changes Room::processEphemeralEvent(EventPtr&& event) @@ -148,7 +148,7 @@ public: namespace _impl { template <typename AlwaysVoid, typename> - struct fn_traits; + struct fn_traits {}; } /// Determine traits of an arbitrary function/lambda/functor diff --git a/libquotient.pri b/libquotient.pri index ef0f112a..677f60d3 100644 --- a/libquotient.pri +++ b/libquotient.pri @@ -72,7 +72,6 @@ HEADERS += \ $$SRCPATH/jobs/syncjob.h \ $$SRCPATH/jobs/mediathumbnailjob.h \ $$SRCPATH/jobs/downloadfilejob.h \ - $$SRCPATH/jobs/postreadmarkersjob.h \ $$files($$SRCPATH/csapi/*.h, false) \ $$files($$SRCPATH/csapi/definitions/*.h, false) \ $$files($$SRCPATH/csapi/definitions/wellknown/*.h, false) \ diff --git a/tests/quotest.cpp b/tests/quotest.cpp index 2b1f0229..a0bad753 100644 --- a/tests/quotest.cpp +++ b/tests/quotest.cpp @@ -186,7 +186,7 @@ TestManager::TestManager(int& argc, char** argv) connect(c, &Connection::connected, this, &TestManager::setupAndRun); connect(c, &Connection::resolveError, this, - [this](const QString& error) { + [](const QString& error) { clog << "Failed to resolve the server: " << error.toStdString() << endl; QCoreApplication::exit(-2); @@ -268,7 +268,7 @@ void TestManager::onNewRoom(Room* r) << endl; connect(r, &Room::aboutToAddNewMessages, r, [r](RoomEventsRange timeline) { clog << timeline.size() << " new event(s) in room " - << r->canonicalAlias().toStdString() << endl; + << r->objectName().toStdString() << endl; }); } @@ -319,13 +319,13 @@ TEST_IMPL(loadMembers) // It's not exactly correct because an arbitrary server might not support // lazy loading; but in the absence of capabilities framework we assume // it does. - if (r->memberNames().size() >= r->joinedCount()) { + if (r->users().size() >= r->joinedCount()) { clog << "Lazy loading doesn't seem to be enabled" << endl; FAIL_TEST(); } r->setDisplayed(); connect(r, &Room::allMembersLoaded, this, [this, thisTest, r] { - FINISH_TEST(r->memberNames().size() >= r->joinedCount()); + FINISH_TEST(r->users().size() >= r->joinedCount()); }); return false; } |