diff --git a/CMakeLists.txt b/CMakeLists.txt index 5387ecab1..5ecba1507 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,7 @@ target_sources(${QUOTIENT_LIB_NAME} PUBLIC FILE_SET HEADERS BASE_DIRS . Quotient/events/eventcontent.h Quotient/events/eventrelation.h Quotient/events/roomcreateevent.h + Quotient/events/roomjoinrulesevent.h Quotient/events/roomtombstoneevent.h Quotient/events/roommessageevent.h Quotient/events/roommemberevent.h diff --git a/Quotient/events/roomjoinrulesevent.h b/Quotient/events/roomjoinrulesevent.h new file mode 100644 index 000000000..6fcf27a19 --- /dev/null +++ b/Quotient/events/roomjoinrulesevent.h @@ -0,0 +1,106 @@ +// SPDX-FileCopyrightText: 2025 James Graham +// SPDX-License-Identifier: LGPL-2.1-or-later + +#pragma once + +#include + +namespace Quotient +{ +namespace EventContent { +Q_NAMESPACE_EXPORT(QUOTIENT_API) + +//! \brief Definition of an allow AllowCondition +//! +//! \sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_ruless +struct AllowCondition { + QString roomId; + QString type; +}; + +//! Enum representing the available room join rules +enum JoinRule { + Public, + Knock, + Invite, + Private, + Restricted, + KnockRestricted, +}; +Q_ENUM_NS(JoinRule) + +[[maybe_unused]] constexpr std::array JoinRuleStrings { + "public"_L1, + "knock"_L1, + "invite"_L1, + "private"_L1, + "restricted"_L1, + "knock_restricted"_L1, +}; + +//! \brief The content of a join rule event +//! +//! \sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules +struct JoinRuleContent { + JoinRule joinRule; + QList allow; +}; +} // namespace EventContent + +template<> +inline EventContent::AllowCondition fromJson(const QJsonObject& jo) +{ + return EventContent::AllowCondition { + fromJson(jo["room_id"_L1]), + fromJson(jo["type"_L1]) + }; +} + +template<> +inline auto toJson(const EventContent::AllowCondition& c) +{ + QJsonObject jo; + addParam(jo, "room_id"_L1, c.roomId); + addParam(jo, "type"_L1, c.type); + return jo; +} + +template<> +inline EventContent::JoinRuleContent fromJson(const QJsonObject& jo) +{ + return EventContent::JoinRuleContent { + enumFromJsonString(jo["join_rule"_L1].toString(), EventContent::JoinRuleStrings).value_or(EventContent::Public), + fromJson>(jo["allow"_L1]) + }; +} + +template<> +inline auto toJson(const EventContent::JoinRuleContent& c) +{ + QJsonObject jo; + addParam(jo, "join_rule"_L1, enumToJsonString(c.joinRule, EventContent::JoinRuleStrings)); + addParam(jo, "allow"_L1, c.allow); + return jo; +} + +//! \brief Class to define a join rule state event. +//! +//! \sa Quotient::StateEvent, https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules +class JoinRulesEvent : public KeylessStateEventBase +{ +public: + QUO_EVENT(JoinRulesEvent, "m.room.join_rules") + using KeylessStateEventBase::KeylessStateEventBase; + + //! \brief The join rule for the room. + //! + //! \sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules + EventContent::JoinRule joinRule() const { return content().joinRule; } + + //! \brief The allow rules for restricted rooms. + //! + //! \sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules + QList allow() const { return content().allow; } +}; +} // namespace Quotient diff --git a/Quotient/room.cpp b/Quotient/room.cpp index 89c1bd49c..099727e91 100644 --- a/Quotient/room.cpp +++ b/Quotient/room.cpp @@ -52,6 +52,7 @@ #include "events/roomavatarevent.h" #include "events/roomcanonicalaliasevent.h" #include "events/roomcreateevent.h" +#include "events/roomjoinrulesevent.h" #include "events/roommemberevent.h" #include "events/roompowerlevelsevent.h" #include "events/roomtombstoneevent.h" @@ -1534,6 +1535,36 @@ RoomStateView Room::currentState() const return d->currentState; } +EventContent::JoinRule Room::joinRule() const +{ + return currentState().queryOr(&JoinRulesEvent::joinRule, EventContent::Public); +} + +QList Room::allowIds() const +{ + QList allowIds; + for (const auto& allowCondition : currentState().queryOr(&JoinRulesEvent::allow, QList())) { + allowIds.append(allowCondition.roomId); + } + return allowIds; +} + +void Room::setJoinRule(EventContent::JoinRule newRule, const QList& allowedRooms) +{ + if (memberEffectivePowerLevel() < powerLevelFor()) { + return; + } + + EventContent::JoinRule actualRule = (newRule == EventContent::Restricted || newRule == EventContent::KnockRestricted) && allowedRooms.isEmpty() ? EventContent::Invite : newRule; + QList newAllow; + for (const auto& room :allowedRooms) { + newAllow.append({room, "m.room_membership"_L1}); + } + setState(actualRule, newAllow); + // Not emitting joinRuleChanged() here, since that would override the change + // in the UI with the *current* value, which is not the *new* value. +} + int Room::memberEffectivePowerLevel(const UserId& memberId) const { return currentState().get()->powerLevelForUser( @@ -3208,6 +3239,14 @@ Room::Change Room::Private::processStateEvent(const RoomEvent& curEvent, return Change::Other; }, + [this, oldEvent](const JoinRulesEvent& evt) { + if (const auto* oldJRE = static_cast(oldEvent); + oldJRE && oldJRE->content().joinRule != evt.content().joinRule + ) { + emit q->joinRuleChanged(); + } + return Change::Other; + }, Change::Other); } diff --git a/Quotient/room.h b/Quotient/room.h index 6b7c28329..7d31943d8 100644 --- a/Quotient/room.h +++ b/Quotient/room.h @@ -25,6 +25,7 @@ #include "events/roommessageevent.h" #include "events/roompowerlevelsevent.h" #include "events/roomtombstoneevent.h" +#include #include #include @@ -165,6 +166,8 @@ class QUOTIENT_API Room : public QObject { Q_PROPERTY(QStringList tagNames READ tagNames NOTIFY tagsChanged) Q_PROPERTY(bool isFavourite READ isFavourite NOTIFY tagsChanged STORED false) Q_PROPERTY(bool isLowPriority READ isLowPriority NOTIFY tagsChanged STORED false) + Q_PROPERTY(EventContent::JoinRule joinRule READ joinRule WRITE setJoinRule NOTIFY joinRuleChanged) + Q_PROPERTY(QList allowIds READ allowIds NOTIFY joinRuleChanged) Q_PROPERTY(GetRoomEventsJob* eventsHistoryJob READ eventsHistoryJob NOTIFY eventsHistoryJobChanged) Q_PROPERTY(int requestedHistorySize READ requestedHistorySize NOTIFY eventsHistoryJobChanged) @@ -672,6 +675,35 @@ class QUOTIENT_API Room : public QObject { /// \brief Get the current room state RoomStateView currentState() const; + //! \brief The current Join Rule for the room + //! + //! \sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules + EventContent::JoinRule joinRule() const; + + //! \brief Set the Join Rule for the room + //! + //! If the local user does not have a high enough power level the request is rejected. + //! + //! \param newRule the new JoinRule to apply to the room + //! \param allowedRooms only required when the join rule is restricted. This is a + //! list of room IDs that members of can join without an invite. + //! If the rule is restricted and this list is empty it is treated as a join + //! rule of invite instead. + //! + //! \note While any room ID is permitted it is designed to be only spaces that are + //! input. I.e. only memebers of space `x` can join this room. + //! + //! \sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules + Q_INVOKABLE void setJoinRule(EventContent::JoinRule newRule, const QList& allowedRooms = {}); + + //! \brief The list of Room IDs for when the join rule is Restricted + //! + //! This value will be empty when the Join Rule is not Restricted or + //! Knock-Restricted. + //! + //! \sa https://spec.matrix.org/latest/client-server-api/#mroomjoin_rules + QList allowIds() const; + //! \brief The effective power level of the given member in the room //! //! This is normally the same as calling `RoomPowerLevelEvent::powerLevelForUser(userId)` but @@ -896,6 +928,9 @@ public Q_SLOTS: void topicChanged(); void avatarChanged(); + //! \brief The join rule for the room has changed + void joinRuleChanged(); + //! \brief A new member has joined the room //! //! This can be from any previous state or a member previously unknown to