From 0a2543f8bd0beea5a9b1b4484c63493e40930da5 Mon Sep 17 00:00:00 2001 From: Alexey Rusakov Date: Sun, 15 Sep 2024 09:10:07 +0200 Subject: [PATCH] Refactor power level event management; Room::powerLevelFor() This commit makes RoomPowerLevelsEvent _always_ available via currentState() - no need to query()/queryOr(), just get() and safely access all the members. This is possible because the spec defines default power levels for all kinds of cases; we can just inject a synthetic RoomPowerLevelsEvent object with those defaults into currentState() and once the real event comes (either from the homeserver or from the cache) it replaces the synthetic one by normal means of state processing. Thanks to that, Room::memberEffectivePowerLevel() implementation becomes much simpler; and to match it on the other side of power level checks, Room::powerLevelFor() is introduced as a unified call returning the power level necessary to send any specific event type, state or not. --- Quotient/room.cpp | 55 ++++++++++++++++++++++++++-------------- Quotient/room.h | 47 ++++++++++++++++++++++++---------- Quotient/roomstateview.h | 3 ++- 3 files changed, 72 insertions(+), 33 deletions(-) diff --git a/Quotient/room.cpp b/Quotient/room.cpp index d3be909e0..20772c9d7 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -94,13 +94,18 @@ class Q_DECL_HIDDEN Room::Private { QString id; JoinState joinState; RoomSummary summary = { {}, 0, {} }; - /// The state of the room at timeline position before-0 + // TODO: remove the below when Room becomes constructed from the first sync batch; a synthetic + // default power levels event would be constructed in baseState then, if needed + //! Fallback when/while the real event is not available + std::unique_ptr defaultPowerLevels = + std::make_unique(); + //! The state of the room at timeline position before-0 std::unordered_map baseState; - /// The state of the room at syncEdge() - /// \sa syncEdge - RoomStateView currentState; - /// Servers with aliases for this room except the one of the local user - /// \sa Room::remoteAliases + //! The state of the room at syncEdge() + //! \sa syncEdge + RoomStateView currentState{ { { RoomPowerLevelsEvent::TypeId, {} }, defaultPowerLevels.get() } }; + //! Servers with aliases for this room except the one of the local user + //! \sa Room::remoteAliases QSet aliasServers; Timeline timeline; @@ -1522,20 +1527,17 @@ RoomStateView Room::currentState() const return d->currentState; } -int Room::memberEffectivePowerLevel(const QString& memberId) const +int Room::memberEffectivePowerLevel(const UserId& memberId) const { - if (!successorId().isEmpty()) { - return 0; // No one can upgrade a room that's already upgraded - } + return currentState().get()->powerLevelForUser( + memberId.isEmpty() ? connection()->userId() : memberId); +} - const auto& mId = memberId.isEmpty() ? connection()->userId() :memberId; - if (const auto* plEvent = currentState().get()) { - return plEvent->powerLevelForUser(mId); - } - if (const auto* createEvent = creation()) { - return createEvent->senderId() == mId ? 100 : 0; - } - return 0; // That's rather weird but may happen, according to rvdh +int Room::powerLevelFor(const QString& eventTypeId, bool forceStateEvent) const +{ + const auto& ple = currentState().get(); + return forceStateEvent || isStateEvent(eventTypeId) ? ple->powerLevelForState(eventTypeId) + : ple->powerLevelForEvent(eventTypeId); } RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent) @@ -1841,7 +1843,8 @@ Room::Changes Room::Private::updateStatsFromSyncData(const SyncRoomData& data, b void Room::updateData(SyncRoomData&& data, bool fromCache) { qCDebug(MAIN) << "--- Updating room" << id() << "/" << objectName(); - bool firstUpdate = d->baseState.empty(); + const bool firstUpdate = d->baseState.empty(); + const bool createEventPreviouslyMissing = creation() == nullptr; if (d->prevBatch && d->prevBatch->isEmpty()) *d->prevBatch = data.timelinePrevBatch; @@ -1862,6 +1865,20 @@ void Room::updateData(SyncRoomData&& data, bool fromCache) roomChanges |= d->updateStatsFromSyncData(data, fromCache); if (roomChanges != 0) { + if (createEventPreviouslyMissing && creation() + && currentState().get() == d->defaultPowerLevels.get()) { + // Handle a special case when RoomCreateEvent just arrived but RoomPowerLevelsEvent + // did not. Usually that means that a power levels event is not in the room at all, + // which is a somewhat extreme but still valid situation. In such a case the spec says + // to rely on the default power levels save for the room creator who is effectively + // allowed to do everything. + // The entire defaultPowerLevels event gets replaced in order to maintain its constness + // everywhere else. + std::exchange(d->defaultPowerLevels, + std::make_unique(PowerLevelsEventContent{ + .users = { { creation()->senderId(), 100 } } })); + } + // First test for changes that can only come from /sync calls and not // other interactions (/members, /messages etc.) if ((roomChanges & Change::Topic) > 0) diff --git a/Quotient/room.h b/Quotient/room.h index f2d86c8e0..720b8d601 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -19,11 +19,12 @@ #include "events/accountdataevents.h" #include "events/encryptedevent.h" +#include "events/eventrelation.h" +#include "events/roomcreateevent.h" #include "events/roomkeyevent.h" #include "events/roommessageevent.h" -#include "events/roomcreateevent.h" +#include "events/roompowerlevelsevent.h" #include "events/roomtombstoneevent.h" -#include "events/eventrelation.h" #include #include @@ -671,20 +672,40 @@ class QUOTIENT_API Room : public QObject { /// \brief Get the current room state RoomStateView currentState() const; - //! \brief The effective power level of the given member in the room. - //! - //! Since a RoomPowerLevels state event may not always be available the following - //! is taken into account in line with the Matrix spec - //! https://spec.matrix.org/v1.8/client-server-api/#mroompower_levels: - //! - The users_default is assumed to be 0. - //! - The room creator is assumed to be 100. + //! \brief The effective power level of the given member in the room //! - //! If \p memberId is empty the power level of the local user will be returned. - //! If the room has been upgraded 0 will be returned to prevent further upgrade attempts. - //! - //! \sa RoomPowerLevelsEvent + //! This is normally the same as calling `RoomPowerLevelEvent::powerLevelForUser(userId)` but + //! takes into account the room context and works even if the room state has no power levels + //! event. It is THE recommended way to get a room member's power level to display in the UI. + //! \param memberId The room member ID to check; if empty, the local user will be checked + //! \sa RoomPowerLevelsEvent, https://spec.matrix.org/v1.11/client-server-api/#mroompower_levels Q_INVOKABLE int memberEffectivePowerLevel(const QString& memberId = {}) const; + //! \brief Get the power level required to send events of the given type + //! + //! \note This is a generic method that only gets the power level to send events with a given + //! type. Some operations have additional restrictions or enablers though: e.g., + //! room member changes (kicks, invites) have special power levels; on the other hand, + //! redactions of one's own messages are allowed regardless of the power level. To check + //! effective ability to perform an operation, use Room's can*() methods instead of + //! comparing the power levels (those are also slightly more efficient). + //! \note Unlike the template version below, this method determines at runtime whether an event + //! type is that of a state event, assuming unknown event types to be non-state; pass + //! `true` as the second parameter to override that. + //! \sa canSend, canRedact, canSwitchVersions + Q_INVOKABLE int powerLevelFor(const QString& eventTypeId, bool forceStateEvent = false) const; + + //! \brief Get the power level required to send events of the given type + //! + //! This is an optimised version of non-template powerLevelFor() (with the same caveat about + //! operations based on some event types) for cases when the event type is known at build time. + //! \tparam EvT the event type to get the power level for + template + int powerLevelFor() const + { + return currentState().get()->powerLevelForEventType(); + } + //! \brief Post a pre-created room message event //! //! Takes ownership of the event, deleting it once the matching one arrives with the sync. diff --git a/Quotient/roomstateview.h b/Quotient/roomstateview.h index 01ac1c087..e1a8b38a3 100644 --- a/Quotient/roomstateview.h +++ b/Quotient/roomstateview.h @@ -205,6 +205,7 @@ class QUOTIENT_API RoomStateView } private: - friend class Room; + friend class Room; // Factory class for RoomStateView + using QHash::QHash; }; } // namespace Quotient