From 8183a33c130f1284404edc61767ff6d29402d200 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 23 Jan 2022 17:11:45 +0100 Subject: RoomStateView This class is called to provide an arbitrary snapshot of a room state; as the first step, Room::currentState() returns an instance of this class that stores, well, the current state. Implelementation-wise it's the same hash map of two-part state event keys to const event pointers; however, RoomStateView provides additional operations: - get(), that deprecates Room::getCurrentState(), returns a pointer to a particular event if the current state has it. Unlike the original method, the pointer returned from this one can be nullptr; this is done to get rid of stubbed state events that have to be created everytime a "state miss" occurred (i.e., when getCurrentState() does not find an existing event in the current state). - eventsOfType() - this is a new place for Room::stateEventsOfType() introduced recently. - query() - this is a way to specify a piece of the state content that you need to retrieve by passing a member function or a function object that retrieves it. That is especially convenient with member functions of the event class; just pass the pointer to this member function, and query() will parse the event type it has to retrieve out of it and call that member function on the event object. Returns an Omittable<>; if the respective piece of state doesn't exist, you'll get `Quotient::none` (the same as `std::nullopt`). - queryOr() - the same but with the fallback value; instead of an Omittable<>, the fallback value will be returned if the needed event is not found. --- lib/room.cpp | 150 ++++++++++++++++++++++++-------------------------- lib/room.h | 54 ++++++++---------- lib/roomstateview.cpp | 35 ++++++++++++ lib/roomstateview.h | 127 ++++++++++++++++++++++++++++++++++++++++++ lib/user.cpp | 17 ++++-- 5 files changed, 267 insertions(+), 116 deletions(-) create mode 100644 lib/roomstateview.cpp create mode 100644 lib/roomstateview.h (limited to 'lib') diff --git a/lib/room.cpp b/lib/room.cpp index ba63f50d..1450eb3b 100644 --- a/lib/room.cpp +++ b/lib/room.cpp @@ -16,6 +16,7 @@ #include "syncdata.h" #include "user.h" #include "eventstats.h" +#include "roomstateview.h" // NB: since Qt 6, moc_room.cpp needs User fully defined #include "moc_room.cpp" @@ -103,7 +104,7 @@ public: static decltype(baseState) stubbedState; /// The state of the room at syncEdge() /// \sa syncEdge - QHash currentState; + RoomStateView currentState; /// Servers with aliases for this room except the one of the local user /// \sa Room::remoteAliases QSet aliasServers; @@ -227,34 +228,6 @@ public: return evt; } - QVector stateEventsOfType(const QString& evtType) const - { - auto vals = QVector(); - for (auto it = currentState.cbegin(); it != currentState.cend(); ++it) - if (it.key().first == evtType) - vals.append(it.value()); - - return vals; - } - - template - const EventT* getCurrentState(const QString& stateKey = {}) const - { - const auto* evt = getCurrentState({ EventT::matrixTypeId(), stateKey }); - Q_ASSERT(evt->type() == EventT::typeId() - && evt->matrixType() == EventT::matrixTypeId()); - return static_cast(evt); - } - -// template -// const auto& getCurrentStateContent(const QString& stateKey = {}) const -// { -// if (const auto* evt = -// currentState.value({ EventT::matrixTypeId(), stateKey }, nullptr)) -// return evt->content(); -// return EventT::content_type() -// } - template Changes updateStateFrom(EventArrayT&& events) { @@ -480,8 +453,8 @@ const QString& Room::id() const { return d->id; } QString Room::version() const { - const auto v = d->getCurrentState()->version(); - return v.isEmpty() ? QStringLiteral("1") : v; + const auto v = currentState().query(&RoomCreateEvent::version); + return v && !v->isEmpty() ? *v : QStringLiteral("1"); } bool Room::isUnstable() const @@ -492,7 +465,10 @@ bool Room::isUnstable() const QString Room::predecessorId() const { - return d->getCurrentState()->predecessor().roomId; + if (const auto* evt = currentState().get()) + return evt->predecessor().roomId; + + return {}; } Room* Room::predecessor(JoinStates statesFilter) const @@ -507,7 +483,8 @@ Room* Room::predecessor(JoinStates statesFilter) const QString Room::successorId() const { - return d->getCurrentState()->successorRoomId(); + return currentState().queryOr(&RoomTombstoneEvent::successorRoomId, + QString()); } Room* Room::successor(JoinStates statesFilter) const @@ -534,39 +511,41 @@ bool Room::allHistoryLoaded() const QString Room::name() const { - return d->getCurrentState()->name(); + return currentState().queryOr(&RoomNameEvent::name, QString()); } QStringList Room::aliases() const { - const auto* evt = d->getCurrentState(); - auto result = evt->altAliases(); - if (!evt->alias().isEmpty()) - result << evt->alias(); - return result; + if (const auto* evt = currentState().get()) { + auto result = evt->altAliases(); + if (!evt->alias().isEmpty()) + result << evt->alias(); + return result; + } + return {}; } QStringList Room::altAliases() const { - return d->getCurrentState()->altAliases(); + return currentState().queryOr(&RoomCanonicalAliasEvent::altAliases, + QStringList()); } QString Room::canonicalAlias() const { - return d->getCurrentState()->alias(); + return currentState().queryOr(&RoomCanonicalAliasEvent::alias, QString()); } QString Room::displayName() const { return d->displayname; } QStringList Room::pinnedEventIds() const { - return d->getCurrentState()->pinnedEvents(); + return currentState().queryOr(&RoomPinnedEvent::pinnedEvents, QStringList()); } QVector Quotient::Room::pinnedEvents() const { - const auto& pinnedIds = d->getCurrentState()->pinnedEvents(); QVector pinnedEvents; - for (auto&& evtId: pinnedIds) + for (const auto& evtId : pinnedEventIds()) if (const auto& it = findInTimeline(evtId); it != historyEdge()) pinnedEvents.append(it->event()); @@ -582,7 +561,7 @@ void Room::refreshDisplayName() { d->updateDisplayname(); } QString Room::topic() const { - return d->getCurrentState()->topic(); + return currentState().queryOr(&RoomTopicEvent::topic, QString()); } QString Room::avatarMediaId() const { return d->avatar.mediaId(); } @@ -621,7 +600,8 @@ JoinState Room::memberJoinState(User* user) const Membership Room::memberState(const QString& userId) const { - return d->getCurrentState(userId)->membership(); + return currentState().queryOr(userId, &RoomMemberEvent::membership, + Membership::Leave); } bool Room::isMember(const QString& userId) const @@ -899,8 +879,9 @@ bool Room::canSwitchVersions() const if (!successorId().isEmpty()) return false; // No one can upgrade a room that's already upgraded - if (const auto* plEvt = d->getCurrentState()) { - const auto currentUserLevel = plEvt->powerLevelForUser(localUser()->id()); + if (const auto* plEvt = currentState().get()) { + const auto currentUserLevel = + plEvt->powerLevelForUser(localUser()->id()); const auto tombstonePowerLevel = plEvt->powerLevelForState("m.room.tombstone"_ls); return currentUserLevel >= tombstonePowerLevel; @@ -1008,6 +989,16 @@ const Room::RelatedEvents Room::relatedEvents( return relatedEvents(evt.id(), relType); } +const RoomCreateEvent* Room::creation() const +{ + return currentState().get(); +} + +const RoomTombstoneEvent *Room::tombstone() const +{ + return currentState().get(); +} + void Room::Private::getAllMembers() { // If already loaded or already loading, there's nothing to do here. @@ -1472,7 +1463,9 @@ int Room::timelineSize() const { return int(d->timeline.size()); } bool Room::usesEncryption() const { - return !d->getCurrentState()->algorithm().isEmpty(); + return !currentState() + .queryOr(&EncryptionEvent::algorithm, QString()) + .isEmpty(); } const StateEventBase* Room::getCurrentState(const QString& evtType, @@ -1481,13 +1474,7 @@ const StateEventBase* Room::getCurrentState(const QString& evtType, return d->getCurrentState({ evtType, stateKey }); } -const QVector -Room::stateEventsOfType(const QString& evtType) const -{ - return d->stateEventsOfType(evtType); -} - -const QHash& Room::currentState() const +RoomStateView Room::currentState() const { return d->currentState; } @@ -1564,7 +1551,7 @@ Room::Changes Room::Private::setSummary(RoomSummary&& newSummary) void Room::Private::insertMemberIntoMap(User* u) { const auto maybeUserName = - getCurrentState(u->id())->newDisplayName(); + currentState.query(u->id(), &RoomMemberEvent::newDisplayName); if (!maybeUserName) qCWarning(MEMBERS) << "insertMemberIntoMap():" << u->id() << "has no name (even empty)"; @@ -1594,9 +1581,9 @@ void Room::Private::insertMemberIntoMap(User* u) void Room::Private::removeMemberFromMap(User* u) { - const auto userName = - getCurrentState( - u->id())->newDisplayName().value_or(QString()); + const auto userName = currentState.queryOr(u->id(), + &RoomMemberEvent::newDisplayName, + QString()); qCDebug(MEMBERS) << "removeMemberFromMap(), username" << userName << "for user" << u->id(); @@ -1679,10 +1666,13 @@ Room::Private::moveEventsToTimeline(RoomEventsRange events, QString Room::memberName(const QString& mxId) const { // See https://github.com/matrix-org/matrix-doc/issues/1375 - const auto rme = getCurrentState(mxId); - return rme->newDisplayName() ? *rme->newDisplayName() - : rme->prevContent() ? rme->prevContent()->displayName.value_or(QString()) - : QString(); + if (const auto rme = currentState().get(mxId)) { + if (rme->newDisplayName()) + return *rme->newDisplayName(); + if (rme->prevContent() && rme->prevContent()->displayName) + return *rme->prevContent()->displayName; + } + return {}; } QString Room::roomMembername(const User* u) const @@ -1737,10 +1727,13 @@ QString Room::htmlSafeMemberName(const QString& userId) const QUrl Room::memberAvatarUrl(const QString &mxId) const { // See https://github.com/matrix-org/matrix-doc/issues/1375 - const auto rme = getCurrentState(mxId); - return rme->newAvatarUrl() ? *rme->newAvatarUrl() - : rme->prevContent() ? rme->prevContent()->avatarUrl.value_or(QUrl()) - : QUrl(); + if (const auto rme = currentState().get(mxId)) { + if (rme->newAvatarUrl()) + return *rme->newAvatarUrl(); + if (rme->prevContent() && rme->prevContent()->avatarUrl) + return *rme->prevContent()->avatarUrl; + } + return {}; } Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, @@ -2489,12 +2482,14 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) auto oldEvent = ti.replaceEvent(makeRedacted(*ti, redaction)); qCDebug(EVENTS) << "Redacted" << oldEvent->id() << "with" << redaction.id(); if (oldEvent->isStateEvent()) { - const StateEventKey evtKey { oldEvent->matrixType(), - oldEvent->stateKey() }; - Q_ASSERT(currentState.contains(evtKey)); - if (currentState.value(evtKey) == oldEvent.get()) { - Q_ASSERT(ti.index() >= 0); // Historical states can't be in - // currentState + // Check whether the old event was a part of current state; if it was, + // update the current state to the redacted event object. + const auto currentStateEvt = + currentState.get(oldEvent->matrixType(), oldEvent->stateKey()); + Q_ASSERT(currentStateEvt); + if (currentStateEvt == oldEvent.get()) { + // Historical states can't be in currentState + Q_ASSERT(ti.index() >= 0); qCDebug(STATE).nospace() << "Redacting state " << oldEvent->matrixType() << "/" << oldEvent->stateKey(); @@ -2514,6 +2509,7 @@ bool Room::Private::processRedaction(const RedactionEvent& redaction) } q->onRedaction(*oldEvent, *ti); emit q->replacedEvent(ti.event(), rawPtr(oldEvent)); + // By now, all references to oldEvent must have been updated to ti.event() return true; } @@ -2754,7 +2750,7 @@ void Room::Private::addHistoricalMessageEvents(RoomEvents&& events) for (const auto& eptr : events) { const auto& e = *eptr; if (e.isStateEvent() - && !currentState.contains({ e.matrixType(), e.stateKey() })) { + && !currentState.contains(e.matrixType(), e.stateKey())) { changes |= q->processStateEvent(e); } } @@ -2791,9 +2787,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) return Change::None; // Find a value (create an empty one if necessary) and get a reference - // to it. Can't use getCurrentState<>() because it (creates and) returns - // a stub if a value is not found, and what's needed here is a "real" event - // or nullptr. + // to it, anticipating a change further in the function. auto& curStateEvent = d->currentState[{ e.matrixType(), e.stateKey() }]; // Prepare for the state change // clang-format off diff --git a/lib/room.h b/lib/room.h index 15bc7648..d49cfb55 100644 --- a/lib/room.h +++ b/lib/room.h @@ -10,6 +10,7 @@ #pragma once #include "connection.h" +#include "roomstateview.h" #include "eventitem.h" #include "quotient_common.h" @@ -399,14 +400,8 @@ public: const RelatedEvents relatedEvents(const RoomEvent& evt, EventRelation::reltypeid_t relType) const; - const RoomCreateEvent* creation() const - { - return getCurrentState(); - } - const RoomTombstoneEvent* tombstone() const - { - return getCurrentState(); - } + const RoomCreateEvent* creation() const; + const RoomTombstoneEvent* tombstone() const; bool displayed() const; /// Mark the room as currently displayed to the user @@ -760,45 +755,40 @@ public: /*! This method returns a (potentially empty) state event corresponding * to the pair of event type \p evtType and state key \p stateKey. */ - Q_INVOKABLE const Quotient::StateEventBase* + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // + const Quotient::StateEventBase* getCurrentState(const QString& evtType, const QString& stateKey = {}) const; - /// Get all state events in the room. - /*! This method returns all known state events that have occured in - * the room, as a mapping from the event type and state key to value. - */ - const QHash& currentState() const; - - /// Get all state events in the room of a certain type. - /*! This method returns all known state events that have occured in - * the room of the given type. - */ - Q_INVOKABLE const QVector - stateEventsOfType(const QString& evtType) const; - /// Get a state event with the given event type and state key /*! This is a typesafe overload that accepts a C++ event type instead of * its Matrix name. */ template + [[deprecated("Use currentState().get() instead; " + "make sure to check its result for nullptrs")]] // const EvT* getCurrentState(const QString& stateKey = {}) const { - const auto* evt = - eventCast(getCurrentState(EvT::matrixTypeId(), stateKey)); + QT_IGNORE_DEPRECATIONS( + const auto* evt = eventCast( + getCurrentState(EvT::matrixTypeId(), stateKey));) Q_ASSERT(evt); Q_ASSERT(evt->matrixTypeId() == EvT::matrixTypeId() && evt->stateKey() == stateKey); return evt; } - /// Set a state event of the given type with the given arguments - /*! This typesafe overload attempts to send a state event with the type - * \p EvT and the content defined by \p args. Specifically, the function - * creates a temporary object of type \p EvT passing \p args to - * the constructor, and sends a request to the homeserver using - * the Matrix event type defined by \p EvT and the event content produced - * via EvT::contentJson(). - */ + /// \brief Get the current room state + RoomStateView currentState() const; + + //! \brief Set a state event of the given type with the given arguments + //! + //! This typesafe overload attempts to send a state event with the type + //! \p EvT and the content defined by \p args. Specifically, the function + //! creates a temporary object of type \p EvT passing \p args to + //! the constructor, and sends a request to the homeserver using + //! the Matrix event type defined by \p EvT and the event content produced + //! via EvT::contentJson(). template auto setState(ArgTs&&... args) const { diff --git a/lib/roomstateview.cpp b/lib/roomstateview.cpp new file mode 100644 index 00000000..94c88eee --- /dev/null +++ b/lib/roomstateview.cpp @@ -0,0 +1,35 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#include "roomstateview.h" + +using namespace Quotient; + +const StateEventBase* RoomStateView::get(const QString& evtType, + const QString& stateKey) const +{ + return value({ evtType, stateKey }); +} + +bool RoomStateView::contains(const QString& evtType, + const QString& stateKey) const +{ + return contains({ evtType, stateKey }); +} + +QJsonObject RoomStateView::contentJson(const QString& evtType, + const QString& stateKey) const +{ + return queryOr(evtType, stateKey, &Event::contentJson, QJsonObject()); +} + +const QVector +RoomStateView::eventsOfType(const QString& evtType) const +{ + auto vals = QVector(); + for (auto it = cbegin(); it != cend(); ++it) + if (it.key().first == evtType) + vals.append(it.value()); + + return vals; +} diff --git a/lib/roomstateview.h b/lib/roomstateview.h new file mode 100644 index 00000000..cab69ae3 --- /dev/null +++ b/lib/roomstateview.h @@ -0,0 +1,127 @@ +// SPDX-FileCopyrightText: 2021 Kitsune Ral +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include "events/stateevent.h" + +#include + +namespace Quotient { + +class Room; + +class RoomStateView : private QHash { + Q_GADGET +public: + const QHash& events() const + { + return *this; + } + + //! \brief Get a state event with the given event type and state key + //! \return A state event corresponding to the pair of event type + //! \p evtType and state key \p stateKey, or nullptr if there's + //! no such \p evtType / \p stateKey combination in the current + //! state. + //! \warning In libQuotient 0.7 the return type changed to an OmittableCref + //! which is effectively a nullable const reference wrapper. You + //! have to check that it has_value() before using. Alternatively + //! you can now use queryCurrentState() to access state safely. + //! \sa getCurrentStateContentJson + const StateEventBase* get(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get a state event with the given event type and state key + //! + //! This is a typesafe overload that accepts a C++ event type instead of + //! its Matrix name. + //! \warning In libQuotient 0.7 the return type changed to an Omittable with + //! a reference wrapper inside - you have to check that it + //! has_value() before using. Alternatively you can now use + //! queryCurrentState() to access state safely. + template + const EvT* get(const QString& stateKey = {}) const + { + static_assert(std::is_base_of_v); + if (const auto* evt = get(EvT::matrixTypeId(), stateKey)) { + Q_ASSERT(evt->matrixType() == EvT::matrixTypeId() + && evt->stateKey() == stateKey); + return eventCast(evt); + } + return nullptr; + } + + using QHash::contains; + + bool contains(const QString& evtType, const QString& stateKey = {}) const; + + template + bool contains(const QString& stateKey = {}) const + { + return contains(EvT::matrixTypeId(), stateKey); + } + + //! \brief Get the content of the current state event with the given + //! event type and state key + //! \return An empty object if there's no event in the current state with + //! this event type and state key; the contents of the event + //! 'content' object otherwise + Q_INVOKABLE QJsonObject contentJson(const QString& evtType, + const QString& stateKey = {}) const; + + //! \brief Get all state events in the room of a certain type. + //! + //! This method returns all known state events that have occured in + //! the room of the given type. + const QVector + eventsOfType(const QString& evtType) const; + + template + auto query(const QString& evtType, const QString& stateKey, FnT&& fn) const + { + return lift(std::forward(fn), get(evtType, stateKey)); + } + + template + auto query(const QString& stateKey, FnT&& fn) const + { + using EventT = std::decay_t>; + static_assert(std::is_base_of_v); + return lift(std::forward(fn), get(stateKey)); + } + + template + auto queryOr(const QString& evtType, const QString& stateKey, FnT&& fn, + FallbackT&& fallback) const + { + return lift(std::forward(fn), get(evtType, stateKey)) + .value_or(std::forward(fallback)); + } + + template + auto query(FnT&& fn) const + { + return query({}, std::forward(fn)); + } + + template + auto queryOr(const QString& stateKey, FnT&& fn, FallbackT&& fallback) const + { + using EventT = std::decay_t>; + static_assert(std::is_base_of_v); + return lift(std::forward(fn), get(stateKey)) + .value_or(std::forward(fallback)); + } + + template + auto queryOr(FnT&& fn, FallbackT&& fallback) const + { + return queryOr({}, std::forward(fn), + std::forward(fallback)); + } + +private: + friend class Room; +}; +} // namespace Quotient diff --git a/lib/user.cpp b/lib/user.cpp index 0dbc444a..f7840c40 100644 --- a/lib/user.cpp +++ b/lib/user.cpp @@ -119,12 +119,17 @@ void User::rename(const QString& newName, const Room* r) return; } // #481: take the current state and update it with the new name - auto evtC = r->getCurrentState(id())->content(); - Q_ASSERT_X(evtC.membership == Membership::Join, __FUNCTION__, - "Attempt to rename a user that's not a room member"); - evtC.displayName = sanitized(newName); - r->setState(id(), move(evtC)); - // The state will be updated locally after it arrives with sync + if (const auto& maybeEvt = r->currentState().get(id())) { + auto content = maybeEvt->content(); + if (content.membership == Membership::Join) { + content.displayName = sanitized(newName); + r->setState(id(), move(content)); + // The state will be updated locally after it arrives with sync + return; + } + } + qCCritical(MEMBERS) + << "Attempt to rename a non-member in a room context - ignored"; } template -- cgit v1.2.3