From 86acad3c4eaaaf2a4a8dc553f6726902d2f16ecf Mon Sep 17 00:00:00 2001 From: Joshua Goins Date: Sat, 22 Feb 2025 12:20:20 -0500 Subject: [PATCH] Add support for profile fields (MSC4133) This adds support for the now-somewhat-supported but still unstable MSC for profile fields. Mostly consisting of jobs for clients, some capability support and logic for determining whether a profile field is supported or not. --- Quotient/connection.cpp | 25 ++++++++++++++++++++++++ Quotient/connection.h | 7 +++++++ Quotient/csapi/capabilities.h | 27 ++++++++++++++++++++++++++ Quotient/csapi/profile.cpp | 16 ++++++++++++++++ Quotient/csapi/profile.h | 36 +++++++++++++++++++++++++++++++++++ 5 files changed, 111 insertions(+) diff --git a/Quotient/connection.cpp b/Quotient/connection.cpp index 9fbb69e12..b26f1646f 100644 --- a/Quotient/connection.cpp +++ b/Quotient/connection.cpp @@ -1972,3 +1972,28 @@ bool Connection::allSessionsSelfVerified(const QString& userId) const database()->execute(query); return !query.next(); } + +bool Connection::canChangeProfileFields() const +{ + return !d->capabilities.profileFields || d->capabilities.profileFields->enabled; +} + +bool Connection::profileFieldAllowed(const QString& key) const +{ + // If the capability is missing, assume we are allowed to edit any profile field + if (!d->capabilities.profileFields) { + return true; + } + + // If it's explicitly in the allow list + if (d->capabilities.profileFields->allowed.contains(key)) { + return true; + } + + // As long as it's not explicitly disallowed + if (d->capabilities.profileFields->disallowed.contains(key)) { + return false; + } + + return true; +} diff --git a/Quotient/connection.h b/Quotient/connection.h index 5fbdf2a5f..18f45f4f7 100644 --- a/Quotient/connection.h +++ b/Quotient/connection.h @@ -145,6 +145,7 @@ class QUOTIENT_API Connection : public QObject { Q_PROPERTY(bool encryptionEnabled READ encryptionEnabled WRITE enableEncryption NOTIFY encryptionChanged) Q_PROPERTY(bool directChatEncryptionEnabled READ directChatEncryptionEnabled WRITE enableDirectChatEncryption NOTIFY directChatsEncryptionChanged) Q_PROPERTY(QStringList accountDataEventTypes READ accountDataEventTypes NOTIFY accountDataChanged) + Q_PROPERTY(bool canChangeProfileFields READ canChangeProfileFields NOTIFY capabilitiesLoaded) public: using UsersToDevicesToContent = QHash>; @@ -449,6 +450,12 @@ class QUOTIENT_API Connection : public QObject { //! \sa loadingCapabilities bool canChangePassword() const; + //! Indicate if the server allows the user to change their profile fields. + bool canChangeProfileFields() const; + + //! Indicate if the server allows this specific profile field. + Q_INVOKABLE bool profileFieldAllowed(const QString& key) const; + //! \brief Check whether encryption is enabled on this connection //! \sa enableEncryption bool encryptionEnabled() const; diff --git a/Quotient/csapi/capabilities.h b/Quotient/csapi/capabilities.h index aeff86d7e..30cf09ec0 100644 --- a/Quotient/csapi/capabilities.h +++ b/Quotient/csapi/capabilities.h @@ -40,6 +40,18 @@ class QUOTIENT_API GetCapabilitiesJob : public BaseJob { QHash available; }; + //! The profile fields the server supports and if they can be edited. + struct QUOTIENT_API ProfileFieldsCapability { + //! If the user is allowed to change their own profile fields. + bool enabled; + + //! A list of allowed profile field keys. + QList allowed; + + //! A list of disallowed profile field keys. + QList disallowed; + }; + //! The custom capabilities the server supports, using the //! Java package naming convention. struct QUOTIENT_API Capabilities { @@ -62,6 +74,10 @@ class QUOTIENT_API GetCapabilitiesJob : public BaseJob { //! account. std::optional getLoginToken{}; + //! Capability to indicate if the user can edit profile fields and which ones they can + //! change. + std::optional profileFields{}; + //! Application-dependent keys using the //! [Common Namespaced Identifier //! Grammar](/appendices/#common-namespaced-identifier-grammar). @@ -96,6 +112,16 @@ struct QUOTIENT_API JsonObjectConverter +struct QUOTIENT_API JsonObjectConverter { + static void fillFrom(const QJsonObject& jo, GetCapabilitiesJob::ProfileFieldsCapability& result) + { + fillFromJson(jo.value("enabled"_L1), result.enabled); + fillFromJson(jo.value("allowed"_L1), result.allowed); + fillFromJson(jo.value("disallowed"_L1), result.disallowed); + } +}; + template <> struct QUOTIENT_API JsonObjectConverter { static void fillFrom(QJsonObject jo, GetCapabilitiesJob::Capabilities& result) @@ -106,6 +132,7 @@ struct QUOTIENT_API JsonObjectConverter { fillFromJson(jo.take("m.set_avatar_url"_L1), result.setAvatarUrl); fillFromJson(jo.take("m.3pid_changes"_L1), result.thirdPartyIdChanges); fillFromJson(jo.take("m.get_login_token"_L1), result.getLoginToken); + fillFromJson(jo.take("uk.tcpip.msc4133.profile_fields"_L1), result.profileFields); fromJson(jo, result.additionalProperties); } }; diff --git a/Quotient/csapi/profile.cpp b/Quotient/csapi/profile.cpp index 7d35fb414..0f53355a9 100644 --- a/Quotient/csapi/profile.cpp +++ b/Quotient/csapi/profile.cpp @@ -53,3 +53,19 @@ GetUserProfileJob::GetUserProfileJob(const QString& userId) : BaseJob(HttpVerb::Get, u"GetUserProfileJob"_s, makePath("/_matrix/client/v3", "/profile/", userId)) {} + +GetProfileFieldJob::GetProfileFieldJob(const QString& userId, const QString& key) + : BaseJob(HttpVerb::Get, u"GetProfileFieldJob"_s, + makePath("/_matrix/client/unstable/uk.tcpip.msc4133", "/profile/", userId, "/", key)) + , m_key(key) +{} + +SetProfileFieldJob::SetProfileFieldJob(const QString& userId, const QString& key, + const QString& value) + : BaseJob(HttpVerb::Put, u"SetProfileFieldJob"_s, + makePath("/_matrix/client/unstable/uk.tcpip.msc4133", "/profile/", userId, "/", key)) +{ + QJsonObject _dataJson; + addParam(_dataJson, key, value); + setRequestData({ _dataJson }); +} diff --git a/Quotient/csapi/profile.h b/Quotient/csapi/profile.h index a96789503..4e42da1d4 100644 --- a/Quotient/csapi/profile.h +++ b/Quotient/csapi/profile.h @@ -84,6 +84,42 @@ class QUOTIENT_API GetAvatarUrlJob : public BaseJob { inline auto collectResponse(const GetAvatarUrlJob* job) { return job->avatarUrl(); } +//! \brief Get a user's profile field. +//! +//! Get one of the user's profile fields. This API may be used to fetch the user's +//! own profile field or to query the profile field of other users; either locally or +//! on remote homeservers. +class QUOTIENT_API GetProfileFieldJob : public BaseJob { +public: + //! \param userId + //! The user whose profile field to query. + //! \param key + //! The key of the profile field. + explicit GetProfileFieldJob(const QString& userId, const QString& key); + + // Result properties + + //! The value of the profile field. + QString value() const { return loadFromJson(m_key); } + +private: + QString m_key; +}; + +//! \brief Sets a user's profile field. +//! +//! Set one of the user's own profile fields. This may fail depending on if the server allows the +//! user to change their own profile field, or if the field isn't allowed. +class QUOTIENT_API SetProfileFieldJob : public BaseJob { +public: + //! \param userId + //! The user whose avatar URL to set. + //! + //! \param avatarUrl + //! The new avatar URL for this user. + explicit SetProfileFieldJob(const QString& userId, const QString& key, const QString& value); +}; + //! \brief Get this user's profile information. //! //! Get the combined profile information for this user. This API may be used