Skip to content

Commit

Permalink
Refactor power level event management; Room::powerLevelFor()
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
KitsuneRal committed Sep 15, 2024
1 parent f220233 commit c57e97a
Show file tree
Hide file tree
Showing 3 changed files with 72 additions and 33 deletions.
55 changes: 36 additions & 19 deletions Quotient/room.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<const RoomPowerLevelsEvent> defaultPowerLevels =
std::make_unique<const RoomPowerLevelsEvent>();
//! The state of the room at timeline position before-0
std::unordered_map<StateEventKey, StateEventPtr> 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<QString> aliasServers;

Timeline timeline;
Expand Down Expand Up @@ -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<RoomPowerLevelsEvent>()->powerLevelForUser(
memberId.isEmpty() ? connection()->userId() : memberId);
}

const auto& mId = memberId.isEmpty() ? connection()->userId() :memberId;
if (const auto* plEvent = currentState().get<RoomPowerLevelsEvent>()) {
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<RoomPowerLevelsEvent>();
return forceStateEvent || isStateEvent(eventTypeId) ? ple->powerLevelForState(eventTypeId)
: ple->powerLevelForEvent(eventTypeId);
}

RoomEventPtr Room::decryptMessage(const EncryptedEvent& encryptedEvent)
Expand Down Expand Up @@ -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;
Expand All @@ -1862,6 +1865,20 @@ void Room::updateData(SyncRoomData&& data, bool fromCache)
roomChanges |= d->updateStatsFromSyncData(data, fromCache);

if (roomChanges != 0) {
if (createEventPreviouslyMissing && creation()
&& currentState().get<RoomPowerLevelsEvent>() == 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<const RoomPowerLevelsEvent>(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)
Expand Down
47 changes: 34 additions & 13 deletions Quotient/room.h
Original file line number Diff line number Diff line change
Expand Up @@ -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 <QtCore/QJsonObject>
#include <QtGui/QImage>
Expand Down Expand Up @@ -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 <EventClass EvT>
int powerLevelFor() const
{
return currentState().get<RoomPowerLevelsEvent>()->powerLevelForEventType<EvT>();
}

//! \brief Post a pre-created room message event
//!
//! Takes ownership of the event, deleting it once the matching one arrives with the sync.
Expand Down
3 changes: 2 additions & 1 deletion Quotient/roomstateview.h
Original file line number Diff line number Diff line change
Expand Up @@ -205,6 +205,7 @@ class QUOTIENT_API RoomStateView
}

private:
friend class Room;
friend class Room; // Factory class for RoomStateView
using QHash<StateEventKey, const StateEvent*>::QHash;
};
} // namespace Quotient

0 comments on commit c57e97a

Please sign in to comment.