diff --git a/Quotient/events/event.cpp b/Quotient/events/event.cpp index 0c7341c95..528058cbc 100644 --- a/Quotient/events/event.cpp +++ b/Quotient/events/event.cpp @@ -4,55 +4,44 @@ #include "event.h" #include "../logging_categories_p.h" -#include "stateevent.h" +#include "../ranges_extras.h" #include +#if Quotient_VERSION_MAJOR == 0 && Quotient_VERSION_MINOR <= 9 +#include "stateevent.h" // For deprecated isStateEvent(); remove, once Event::isStateEvent() is gone +#endif + using namespace Quotient; void AbstractEventMetaType::addDerived(const AbstractEventMetaType* newType) { if (const auto existing = - std::find_if(derivedTypes.cbegin(), derivedTypes.cend(), - [&newType](const AbstractEventMetaType* t) { - return t->matrixId == newType->matrixId; - }); - existing != derivedTypes.cend()) - { + findIndirect(_derivedTypes, newType->matrixId, &AbstractEventMetaType::matrixId); + existing != _derivedTypes.cend()) { if (*existing == newType) return; // Two different metatype objects claim the same Matrix type id; this // is not normal, so give as much information as possible to diagnose if ((*existing)->className == newType->className) { - qCritical(EVENTS) - << newType->className << "claims" << newType->matrixId - << "repeatedly; check that it's exported across translation " - "units or shared objects"; + qCritical(EVENTS) << newType->className << "claims" << newType->matrixId + << "repeatedly; check that it's exported across translation " + "units or shared objects"; Q_ASSERT(false); // That situation is plain wrong return; // So maybe std::terminate() even? } - qWarning(EVENTS).nospace() - << newType->matrixId << " is already mapped to " - << (*existing)->className << " before " << newType->className - << "; unless the two have different isValid() conditions, the " - "latter class will never be used"; + qWarning(EVENTS).nospace() << newType->matrixId << " is already mapped to " + << (*existing)->className << " before " << newType->className + << "; unless the two have different isValid() conditions, the " + "latter class will never be used"; } - derivedTypes.emplace_back(newType); - qDebug(EVENTS).nospace() - << newType->matrixId << " -> " << newType->className << "; " - << derivedTypes.size() << " derived type(s) registered for " - << className; + _derivedTypes.emplace_back(newType); + qDebug(EVENTS).nospace() << newType->matrixId << " -> " << newType->className << "; " + << _derivedTypes.size() << " derived type(s) registered for " + << className; } -Event::Event(const QJsonObject& json) - : _json(json) -{ - if (!json.contains(ContentKey) - && !json.value(UnsignedKey).toObject().contains(RedactedCauseKey)) { - qCWarning(EVENTS) << "Event without 'content' node"; - qCWarning(EVENTS) << formatJson << json; - } -} +Event::Event(const QJsonObject& json) : _json(json) {} Event::~Event() = default; @@ -68,7 +57,9 @@ const QJsonObject Event::unsignedJson() const return fullJson()[UnsignedKey].toObject(); } +#if Quotient_VERSION_MAJOR == 0 && Quotient_VERSION_MINOR <= 9 bool Event::isStateEvent() const { return is(); } +#endif void Event::dumpTo(QDebug dbg) const { diff --git a/Quotient/events/event.h b/Quotient/events/event.h index 2d067b7f5..d94e1711f 100644 --- a/Quotient/events/event.h +++ b/Quotient/events/event.h @@ -3,9 +3,12 @@ #pragma once +#include "single_key_value.h" + #include #include -#include "single_key_value.h" + +#include namespace Quotient { // === event_ptr_tt<> and basic type casting facilities === @@ -19,6 +22,7 @@ constexpr inline auto TypeKey = "type"_L1; constexpr inline auto BodyKey = "body"_L1; constexpr inline auto ContentKey = "content"_L1; constexpr inline auto SenderKey = "sender"_L1; +constexpr inline auto UnsignedKey = "unsigned"_L1; using event_type_t = QLatin1String; @@ -55,6 +59,7 @@ class QUOTIENT_API AbstractEventMetaType { } void addDerived(const AbstractEventMetaType* newType); + auto derivedTypes() const { return std::span(_derivedTypes); } virtual ~AbstractEventMetaType() = default; @@ -69,7 +74,7 @@ class QUOTIENT_API AbstractEventMetaType { Event*& event) const = 0; private: - std::vector derivedTypes{}; + std::vector _derivedTypes{}; Q_DISABLE_COPY_MOVE(AbstractEventMetaType) }; @@ -146,7 +151,7 @@ class QUOTIENT_API EventMetaType : public AbstractEventMetaType { if (EventT::TypeId != type) return false; } else { - for (const auto& p : derivedTypes) { + for (const auto& p : _derivedTypes) { p->doLoadFrom(fullJson, type, event); if (event) { Q_ASSERT(is(*event)); @@ -328,10 +333,9 @@ class QUOTIENT_API Event { return dbg; } - // State events are quite special in Matrix; so isStateEvent() is here, - // as an exception. For other base events, Event::is<>() and - // Quotient::is<>() should be used; don't add is* methods here - bool isStateEvent() const; +#if Quotient_VERSION_MAJOR == 0 && Quotient_VERSION_MINOR <= 9 + [[deprecated("isStateEvent() has moved to RoomEvent")]] bool isStateEvent() const; +#endif protected: friend class EventMetaType; // To access the below constructor diff --git a/Quotient/events/roomevent.cpp b/Quotient/events/roomevent.cpp index 051e4d88a..fb392b1a4 100644 --- a/Quotient/events/roomevent.cpp +++ b/Quotient/events/roomevent.cpp @@ -3,10 +3,11 @@ #include "roomevent.h" -#include "../logging_categories_p.h" +#include "encryptedevent.h" #include "redactionevent.h" +#include "stateevent.h" -#include "encryptedevent.h" +#include "../logging_categories_p.h" using namespace Quotient; @@ -48,6 +49,8 @@ QString RoomEvent::transactionId() const return unsignedPart("transaction_id"_L1); } +bool RoomEvent::isStateEvent() const { return is(); } + QString RoomEvent::stateKey() const { return fullJson()[StateKeyKey].toString(); @@ -98,3 +101,17 @@ const QJsonObject RoomEvent::encryptedJson() const } return _originalEvent->fullJson(); } + +namespace { +bool containsEventType(const auto& haystack, const auto& needle) +{ + return std::ranges::any_of(haystack, [needle](const AbstractEventMetaType* candidate) { + return candidate->matrixId == needle || containsEventType(candidate->derivedTypes(), needle); + }); +} +} + +bool Quotient::isStateEvent(const QString& eventTypeId) +{ + return containsEventType(StateEvent::BaseMetaType.derivedTypes(), eventTypeId); +} diff --git a/Quotient/events/roomevent.h b/Quotient/events/roomevent.h index fe71e3d8b..4f013e54e 100644 --- a/Quotient/events/roomevent.h +++ b/Quotient/events/roomevent.h @@ -14,7 +14,6 @@ constexpr inline auto RoomIdKey = "room_id"_L1; constexpr inline auto StateKeyKey = "state_key"_L1; constexpr inline auto RedactedCauseKey = "redacted_because"_L1; constexpr inline auto RelatesToKey = "m.relates_to"_L1; -constexpr inline auto UnsignedKey = "unsigned"_L1; class RedactionEvent; class EncryptedEvent; @@ -49,6 +48,12 @@ class QUOTIENT_API RoomEvent : public Event { //! The transaction_id JSON value for the event. QString transactionId() const; + // State events are special in Matrix; so isStateEvent() and stateKey() are here, + // as an exception. For other event types (including base types), Event::is<>() and + // Quotient::is<>() should be used + + bool isStateEvent() const; + QString stateKey() const; //! \brief Fill the pending event object with the room id @@ -89,6 +94,9 @@ using RoomEventPtr = event_ptr_tt; using RoomEvents = EventsArray; using RoomEventsRange = std::ranges::subrange; +//! \brief Determine whether a given event type is that of a state event +QUOTIENT_API bool isStateEvent(const QString& eventTypeId); + } // namespace Quotient Q_DECLARE_METATYPE(Quotient::RoomEvent*) Q_DECLARE_METATYPE(const Quotient::RoomEvent*) diff --git a/Quotient/events/roompowerlevelsevent.cpp b/Quotient/events/roompowerlevelsevent.cpp index 0a68b6044..63b42d26a 100644 --- a/Quotient/events/roompowerlevelsevent.cpp +++ b/Quotient/events/roompowerlevelsevent.cpp @@ -5,33 +5,36 @@ using namespace Quotient; -// The default values used below are defined in -// https://spec.matrix.org/v1.3/client-server-api/#mroompower_levels -PowerLevelsEventContent::PowerLevelsEventContent(const QJsonObject& json) : - invite(json["invite"_L1].toInt(50)), - kick(json["kick"_L1].toInt(50)), - ban(json["ban"_L1].toInt(50)), - redact(json["redact"_L1].toInt(50)), - events(fromJson>(json["events"_L1])), - eventsDefault(json["events_default"_L1].toInt(0)), - stateDefault(json["state_default"_L1].toInt(50)), - users(fromJson>(json["users"_L1])), - usersDefault(json["users_default"_L1].toInt(0)), - notifications(Notifications{json["notifications"_L1].toObject()["room"_L1].toInt(50)}) -{} - -QJsonObject PowerLevelsEventContent::toJson() const +PowerLevelsEventContent JsonConverter::load(const QJsonValue& jv) { - return QJsonObject{ { u"invite"_s, invite }, - { u"kick"_s, kick }, - { u"ban"_s, ban }, - { u"redact"_s, redact }, - { u"events"_s, Quotient::toJson(events) }, - { u"events_default"_s, eventsDefault }, - { u"state_default"_s, stateDefault }, - { u"users"_s, Quotient::toJson(users) }, - { u"users_default"_s, usersDefault }, - { u"notifications"_s, QJsonObject{ { u"room"_s, notifications.room } } } }; + const auto& jo = jv.toObject(); + PowerLevelsEventContent c; +#define FROM_JSON(member) fromJson(jo[toSnakeCase(#member##_L1)], c.member) + FROM_JSON(invite); + FROM_JSON(kick); + FROM_JSON(ban); + FROM_JSON(redact); + FROM_JSON(events); + FROM_JSON(eventsDefault); + FROM_JSON(stateDefault); + FROM_JSON(users); + FROM_JSON(usersDefault); + fromJson(jo["notifications"_L1]["room"_L1], c.notifications.room); +#undef FROM_JSON + return c; +} + +QJsonObject JsonConverter::dump(const PowerLevelsEventContent& c) { + return QJsonObject{ { u"invite"_s, c.invite }, + { u"kick"_s, c.kick }, + { u"ban"_s, c.ban }, + { u"redact"_s, c.redact }, + { u"events"_s, Quotient::toJson(c.events) }, + { u"events_default"_s, c.eventsDefault }, + { u"state_default"_s, c.stateDefault }, + { u"users"_s, Quotient::toJson(c.users) }, + { u"users_default"_s, c.usersDefault }, + { u"notifications"_s, QJsonObject{ { u"room"_s, c.notifications.room } } } }; } int RoomPowerLevelsEvent::powerLevelForEvent(const QString& eventTypeId) const diff --git a/Quotient/events/roompowerlevelsevent.h b/Quotient/events/roompowerlevelsevent.h index aedc7a725..56191f683 100644 --- a/Quotient/events/roompowerlevelsevent.h +++ b/Quotient/events/roompowerlevelsevent.h @@ -6,28 +6,33 @@ #include "stateevent.h" namespace Quotient { + struct QUOTIENT_API PowerLevelsEventContent { - struct Notifications { - int room; - }; + // See https://spec.matrix.org/v1.11/client-server-api/#mroompower_levels for the defaults - explicit PowerLevelsEventContent(const QJsonObject& json); - QJsonObject toJson() const; + int invite = 0; + int kick = 50; + int ban = 50; - int invite; - int kick; - int ban; + int redact = 50; - int redact; + QHash events{}; + int eventsDefault = 0; + int stateDefault = 50; - QHash events; - int eventsDefault; - int stateDefault; + QHash users{}; + int usersDefault = 0; - QHash users; - int usersDefault; + struct Notifications { + int room = 50; + }; + Notifications notifications{}; +}; - Notifications notifications; +template <> +struct QUOTIENT_API JsonConverter { + static PowerLevelsEventContent load(const QJsonValue& jv); + static QJsonObject dump(const PowerLevelsEventContent& c); }; class QUOTIENT_API RoomPowerLevelsEvent @@ -52,8 +57,37 @@ class QUOTIENT_API RoomPowerLevelsEvent int roomNotification() const { return content().notifications.room; } + //! \brief Get the power level for message events of a given type + //! + //! \note You normally should not compare power levels returned from this + //! and other powerLevelFor*() functions directly; use + //! Room::canSendEvents() instead int powerLevelForEvent(const QString& eventTypeId) const; + + //! \brief Get the power level for state events of a given type + //! + //! \note You normally should not compare power levels returned from this + //! and other powerLevelFor*() functions directly; use + //! Room::canSetState() instead int powerLevelForState(const QString& eventTypeId) const; + + //! \brief Get the power level for a given user + //! + //! \note You normally should not directly use power levels returned by this + //! and other powerLevelFor*() functions; use Room API instead + //! \sa Room::canSend, Room::canSendEvents, Room::canSetState, + //! Room::effectivePowerLevel int powerLevelForUser(const QString& userId) const; + + template + int powerLevelForEventType() const + { + if constexpr (std::is_base_of_v) { + return powerLevelForState(EvT::TypeId); + } else { + return powerLevelForEvent(EvT::TypeId); + } + } }; + } // namespace Quotient diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 8a461b6e9..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) @@ -3078,8 +3095,7 @@ Room::Changes Room::processStateEvent(const RoomEvent& e) Q_ASSERT(result != Change::None); // Whatever the outcome, the relevant piece of state should stay valid // (the absense of event is a valid state, too) - Q_ASSERT(currentState().queryOr(e.matrixType(), e.stateKey(), - &Event::isStateEvent, true)); + Q_ASSERT(currentState().queryOr(e.matrixType(), e.stateKey(), &RoomEvent::isStateEvent, true)); return result; } 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