From 3de943e50fbc7dfb320b2414121736dc99c907c2 Mon Sep 17 00:00:00 2001 From: James Graham Date: Sun, 12 May 2024 18:22:27 +0000 Subject: [PATCH] Set Identity server Part of network/neochat#565 --- src/CMakeLists.txt | 2 + src/identityserverhelper.cpp | 154 ++++++++++++++++++++++++ src/identityserverhelper.h | 103 ++++++++++++++++ src/settings/AccountEditorPage.qml | 9 ++ src/settings/CMakeLists.txt | 1 + src/settings/IdentityServerDelegate.qml | 142 ++++++++++++++++++++++ 6 files changed, 411 insertions(+) create mode 100644 src/identityserverhelper.cpp create mode 100644 src/identityserverhelper.h create mode 100644 src/settings/IdentityServerDelegate.qml diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8a73a375c..2721bd463 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -179,6 +179,8 @@ add_library(neochat STATIC threepidaddhelper.h jobs/neochatadd3pidjob.cpp jobs/neochatadd3pidjob.h + identityserverhelper.cpp + identityserverhelper.h ) set_source_files_properties(qml/OsmLocationPlugin.qml PROPERTIES diff --git a/src/identityserverhelper.cpp b/src/identityserverhelper.cpp new file mode 100644 index 000000000..1e84f7f8d --- /dev/null +++ b/src/identityserverhelper.cpp @@ -0,0 +1,154 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#include "identityserverhelper.h" + +#include + +#include + +#include + +#include "neochatconnection.h" + +IdentityServerHelper::IdentityServerHelper(QObject *parent) + : QObject(parent) +{ +} + +NeoChatConnection *IdentityServerHelper::connection() const +{ + return m_connection; +} + +void IdentityServerHelper::setConnection(NeoChatConnection *connection) +{ + if (m_connection == connection) { + return; + } + + if (m_connection != nullptr) { + m_connection->disconnect(this); + } + + m_connection = connection; + Q_EMIT connectionChanged(); + Q_EMIT currentServerChanged(); + + connect(m_connection, &NeoChatConnection::accountDataChanged, this, [this](const QString &type) { + if (type == QLatin1String("m.identity_server")) { + Q_EMIT currentServerChanged(); + } + }); +} + +QString IdentityServerHelper::currentServer() const +{ + if (m_connection == nullptr) { + return {}; + } + + if (!m_connection->hasAccountData(QLatin1String("m.identity_server"))) { + return i18nc("@info", "No identity server configured"); + } + + const auto url = m_connection->accountData(QLatin1String("m.identity_server"))->contentPart(QLatin1String("base_url")); + if (!url.isEmpty()) { + return url.toString(); + } + return i18nc("@info", "No identity server configured"); +} + +bool IdentityServerHelper::hasCurrentServer() const +{ + if (m_connection == nullptr && !m_connection->hasAccountData(QLatin1String("m.identity_server"))) { + return false; + } + + const auto url = m_connection->accountData(QLatin1String("m.identity_server"))->contentPart(QLatin1String("base_url")); + if (!url.isEmpty()) { + return true; + } + return false; +} + +QString IdentityServerHelper::url() const +{ + return m_url; +} + +void IdentityServerHelper::setUrl(const QString &url) +{ + if (url == m_url) { + return; + } + m_url = url; + Q_EMIT urlChanged(); + + checkUrl(); +} + +IdentityServerHelper::IdServerStatus IdentityServerHelper::status() const +{ + return m_status; +} + +void IdentityServerHelper::checkUrl() +{ + if (m_idServerCheckRequest != nullptr) { + m_idServerCheckRequest->abort(); + m_idServerCheckRequest.clear(); + } + + if (m_url == currentServer()) { + m_status = Match; + Q_EMIT statusChanged(); + return; + } + + if (m_url.isEmpty()) { + m_status = Valid; + Q_EMIT statusChanged(); + return; + } + + const auto requestUrl = QUrl(m_url + QStringLiteral("/_matrix/identity/v2")); + if (!(requestUrl.scheme() == QStringLiteral("https") || requestUrl.scheme() == QStringLiteral("http"))) { + m_status = Invalid; + Q_EMIT statusChanged(); + return; + } + + QNetworkRequest request(requestUrl); + m_idServerCheckRequest = Quotient::NetworkAccessManager::instance()->get(request); + connect(m_idServerCheckRequest, &QNetworkReply::finished, this, [this]() { + if (m_idServerCheckRequest->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 200) { + m_status = Valid; + Q_EMIT statusChanged(); + } else { + m_status = Invalid; + Q_EMIT statusChanged(); + } + }); +} + +void IdentityServerHelper::setIdentityServer() +{ + if (m_url == currentServer()) { + return; + } + + m_connection->setAccountData(QLatin1String("m.identity_server"), {{QLatin1String("base_url"), m_url}}); + m_status = Ready; + Q_EMIT statusChanged(); +} + +void IdentityServerHelper::clearIdentityServer() +{ + if (currentServer().isEmpty()) { + return; + } + m_connection->setAccountData(QLatin1String("m.identity_server"), {{QLatin1String("base_url"), QString()}}); + m_status = Ready; + Q_EMIT statusChanged(); +} diff --git a/src/identityserverhelper.h b/src/identityserverhelper.h new file mode 100644 index 000000000..3e94d3c3d --- /dev/null +++ b/src/identityserverhelper.h @@ -0,0 +1,103 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +#pragma once + +#include +#include + +#include + +class NeoChatConnection; + +/** + * @class IdentityServerHelper + * + * This class is designed to help the process of setting an identity server for the account. + * It will manage the various stages of verification and authentication. + */ +class IdentityServerHelper : public QObject +{ + Q_OBJECT + QML_ELEMENT + + /** + * @brief The connection to add a 3PID to. + */ + Q_PROPERTY(NeoChatConnection *connection READ connection WRITE setConnection NOTIFY connectionChanged) + + /** + * @brief The current identity server. + */ + Q_PROPERTY(QString currentServer READ currentServer NOTIFY currentServerChanged) + + /** + * @brief Whether an identity server is currently configured. + */ + Q_PROPERTY(bool hasCurrentServer READ hasCurrentServer NOTIFY currentServerChanged) + + /** + * @brief The URL for the desired server. + */ + Q_PROPERTY(QString url READ url WRITE setUrl NOTIFY urlChanged) + + /** + * @brief The current status. + */ + Q_PROPERTY(IdServerStatus status READ status NOTIFY statusChanged) + +public: + /** + * @brief The current status for adding an identity server + */ + enum IdServerStatus { + Ready, /**< The process is ready to start. I.e. there is no ongoing attempt to set a new server. */ + Valid, /**< The server URL is valid. */ + Invalid, /**< The server URL is invalid. */ + Match, /**< The server URL is the one that is already configured. */ + Other, /**< An unknown problem occurred. */ + }; + Q_ENUM(IdServerStatus) + + explicit IdentityServerHelper(QObject *parent = nullptr); + + [[nodiscard]] NeoChatConnection *connection() const; + void setConnection(NeoChatConnection *connection); + + [[nodiscard]] QString currentServer() const; + + [[nodiscard]] bool hasCurrentServer() const; + + [[nodiscard]] QString url() const; + void setUrl(const QString &url); + + [[nodiscard]] IdServerStatus status() const; + + /** + * @brief Set the current URL as the user's identity server. + * + * Will do nothing if the URL isn't a valid identity server. + */ + Q_INVOKABLE void setIdentityServer(); + + /** + * @brief Clear the user's identity server. + */ + Q_INVOKABLE void clearIdentityServer(); + +Q_SIGNALS: + void connectionChanged(); + void currentServerChanged(); + void urlChanged(); + void statusChanged(); + +private: + QPointer m_connection; + + IdServerStatus m_status = Ready; + QString m_url; + + QPointer m_idServerCheckRequest; + + void checkUrl(); +}; diff --git a/src/settings/AccountEditorPage.qml b/src/settings/AccountEditorPage.qml index c2ec02454..9fcddba59 100644 --- a/src/settings/AccountEditorPage.qml +++ b/src/settings/AccountEditorPage.qml @@ -207,6 +207,15 @@ FormCard.FormCardPage { title: i18n("Phone Numbers") medium: "msisdn" } + FormCard.FormHeader { + Layout.fillWidth: true + title: i18n("Identity Server") + } + FormCard.FormCard { + IdentityServerDelegate { + connection: root.connection + } + } FormCard.FormHeader { Layout.fillWidth: true title: i18n("Server Information") diff --git a/src/settings/CMakeLists.txt b/src/settings/CMakeLists.txt index 6b183c542..f45c4fd19 100644 --- a/src/settings/CMakeLists.txt +++ b/src/settings/CMakeLists.txt @@ -29,6 +29,7 @@ qt_add_qml_module(settings DevicesCard.qml DeviceDelegate.qml EmoticonFormCard.qml + IdentityServerDelegate.qml IgnoredUsersDialog.qml NotificationRuleItem.qml PasswordSheet.qml diff --git a/src/settings/IdentityServerDelegate.qml b/src/settings/IdentityServerDelegate.qml new file mode 100644 index 000000000..e9788a12a --- /dev/null +++ b/src/settings/IdentityServerDelegate.qml @@ -0,0 +1,142 @@ +// SPDX-FileCopyrightText: 2024 James Graham +// SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts + +import org.kde.kirigami as Kirigami +import org.kde.kirigamiaddons.formcard as FormCard + +import org.kde.neochat + +FormCard.AbstractFormDelegate { + id: root + + required property NeoChatConnection connection + + property bool editServerUrl: false + + text: identityServerHelper.currentServer + + onClicked: editIdServerButton.toggle() + + contentItem: RowLayout { + spacing: Kirigami.Units.largeSpacing + + QQC2.Label { + Layout.fillWidth: true + visible: !root.editServerUrl + text: root.text + elide: Text.ElideRight + } + ColumnLayout { + Kirigami.ActionTextField { + id: editUrlField + Layout.fillWidth: true + visible: root.editServerUrl + + Accessible.description: i18n("New identity server url") + + rightActions: [ + Kirigami.Action { + text: i18nc("@action:button", "Cancel editing identity server url") + icon.name: "edit-delete-remove" + onTriggered: editIdServerButton.toggle() + }, + Kirigami.Action { + enabled: identityServerHelper.status == IdentityServerHelper.Valid + text: i18nc("@action:button", "Confirm new identity server url") + icon.name: "checkmark" + visible: editUrlField.text !== root.text + onTriggered: { + identityServerHelper.setIdentityServer(); + editUrlField.text = ""; + editIdServerButton.toggle(); + } + } + ] + + onAccepted: { + identityServerHelper.setIdentityServer() + editUrlField.text = ""; + editIdServerButton.toggle(); + } + } + Kirigami.InlineMessage { + id: editUrlStatus + visible: root.editServerUrl && text.length > 0 && !warningTimer.running + Layout.topMargin: visible ? Kirigami.Units.smallSpacing : 0 + Layout.fillWidth: true + text: switch(identityServerHelper.status) { + case IdentityServerHelper.Invalid: + return i18n("The entered url is not a valid identity server"); + case IdentityServerHelper.Match: + return i18n("The entered url is already configured as your identity server"); + default: + return ""; + } + + type: switch(identityServerHelper.status) { + case IdentityServerHelper.Invalid: + return Kirigami.MessageType.Error; + case IdentityServerHelper.Match: + return Kirigami.MessageType.Warning; + default: + return Kirigami.MessageType.Information; + } + + Timer { + id: warningTimer + interval: 500 + } + } + } + QQC2.ToolButton { + id: editIdServerButton + display: QQC2.AbstractButton.IconOnly + text: i18nc("@action:button", "Edit identity server url") + icon.name: "document-edit" + checkable: true + onCheckedChanged: { + root.editServerUrl = !root.editServerUrl; + if (checked) { + editUrlField.forceActiveFocus(); + } else { + editUrlField.text = ""; + } + } + QQC2.ToolTip { + text: editIdServerButton.text + delay: Kirigami.Units.toolTipDelay + visible: editIdServerButton.hovered + } + } + QQC2.ToolButton { + id: removeIdServerButton + visible: identityServerHelper.hasCurrentServer + display: QQC2.AbstractButton.IconOnly + text: i18nc("@action:button", "Remove identity server") + icon.name: "edit-delete-remove" + onClicked: { + identityServerHelper.clearIdentityServer(); + editUrlField.text = ""; + if (editIdServerButton.checked) { + editIdServerButton.toggle(); + } + } + QQC2.ToolTip { + text: removeIdServerButton.text + delay: Kirigami.Units.toolTipDelay + visible: removeIdServerButton.hovered + } + } + } + + IdentityServerHelper { + id: identityServerHelper + connection: root.connection + url: editUrlField.text + onUrlChanged: warningTimer.restart() + } +}