From 59164d3bb2334f3cda52608b62c0b96c15c309c7 Mon Sep 17 00:00:00 2001 From: Tobias Fella Date: Mon, 28 Aug 2023 10:05:09 +0000 Subject: [PATCH] Implement account registration Implements #7 --- CMakeLists.txt | 2 +- android/AndroidManifest.xml | 2 +- src/CMakeLists.txt | 7 + src/main.cpp | 10 + src/qml/Component/Login/Captcha.qml | 49 +++ src/qml/Component/Login/Email.qml | 59 ++++ src/qml/Component/Login/Homeserver.qml | 66 ++-- src/qml/Component/Login/Login.qml | 11 +- src/qml/Component/Login/LoginRegister.qml | 3 + src/qml/Component/Login/LoginStep.qml | 2 +- src/qml/Component/Login/RegisterPassword.qml | 51 +++ src/qml/Component/Login/Terms.qml | 39 +++ src/qml/Component/Login/Username.qml | 46 +++ src/qml/Page/WelcomePage.qml | 27 +- src/registration.cpp | 338 +++++++++++++++++++ src/registration.h | 163 +++++++++ src/res.qrc | 7 +- 17 files changed, 829 insertions(+), 53 deletions(-) create mode 100644 src/qml/Component/Login/Captcha.qml create mode 100644 src/qml/Component/Login/Email.qml create mode 100644 src/qml/Component/Login/RegisterPassword.qml create mode 100644 src/qml/Component/Login/Terms.qml create mode 100644 src/qml/Component/Login/Username.qml create mode 100644 src/registration.cpp create mode 100644 src/registration.h diff --git a/CMakeLists.txt b/CMakeLists.txt index 9ccbfdcec..8cc431804 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -77,7 +77,7 @@ ecm_setup_version(${PROJECT_VERSION} VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h ) -find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg) +find_package(Qt${QT_MAJOR_VERSION} ${QT_MIN_VERSION} NO_MODULE COMPONENTS Core Quick Gui QuickControls2 Multimedia Svg WebView) set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES TYPE REQUIRED PURPOSE "Basic application components" diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml index f58fe103b..aab27b4f2 100644 --- a/android/AndroidManifest.xml +++ b/android/AndroidManifest.xml @@ -9,7 +9,7 @@ android:versionName="${versionName}" android:versionCode="${versionCode}" android:installLocation="auto"> - + #endif +#ifdef HAVE_WEBVIEW +#include +#endif + #include #ifdef HAVE_KDBUSADDONS #include @@ -95,6 +99,7 @@ #include "runner.h" #include #endif +#include "registration.h" #ifdef Q_OS_WINDOWS #include @@ -139,6 +144,10 @@ int main(int argc, char *argv[]) QNetworkProxyFactory::setUseSystemConfiguration(true); +#ifdef HAVE_WEBVIEW + QtWebView::initialize(); +#endif + #ifdef Q_OS_ANDROID QGuiApplication app(argc, argv); QQuickStyle::setStyle(QStringLiteral("org.kde.breeze")); @@ -233,6 +242,7 @@ int main(int argc, char *argv[]) qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "AccountRegistry", &Controller::instance().accounts()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance()); + qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Registration", &Registration::instance()); qmlRegisterType("org.kde.neochat", 1, 0, "ActionsHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "ChatDocumentHandler"); qmlRegisterType("org.kde.neochat", 1, 0, "RoomListModel"); diff --git a/src/qml/Component/Login/Captcha.qml b/src/qml/Component/Login/Captcha.qml new file mode 100644 index 000000000..200a7171a --- /dev/null +++ b/src/qml/Component/Login/Captcha.qml @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.14 +import QtQuick.Controls 2.14 as Controls +import QtQuick.Layouts 1.14 +import QtWebView 1.15 + +import org.kde.kirigami 2.12 as Kirigami +import org.kde.kirigamiaddons.formcard 1.0 as FormCard + +import org.kde.neochat 1.0 + +LoginStep { + id: root + + FormCard.AbstractFormDelegate { + background: null + contentItem: WebView { + id: webview + url: "http://localhost:20847" + implicitHeight: 500 + onLoadingChanged: { + webview.runJavaScript("document.body.style.background = '" + Kirigami.Theme.backgroundColor + "'") + } + + Timer { + id: timer + repeat: true + running: true + interval: 300 + onTriggered: { + if(!webview.visible) { + return + } + webview.runJavaScript("!!grecaptcha ? grecaptcha.getResponse() : \"\"", function(response){ + if(!webview.visible || !response) + return + timer.running = false; + Registration.recaptchaResponse = response; + }) + } + } + } + } + previousAction: Kirigami.Action { + onTriggered: root.processed("qrc:/Username.qml") + } +} diff --git a/src/qml/Component/Login/Email.qml b/src/qml/Component/Login/Email.qml new file mode 100644 index 000000000..b41d0b95f --- /dev/null +++ b/src/qml/Component/Login/Email.qml @@ -0,0 +1,59 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.12 + +import org.kde.kirigami 2.12 as Kirigami +import org.kde.kirigamiaddons.formcard 1.0 as FormCard + +import org.kde.neochat 1.0 + +LoginStep { + id: root + + onActiveFocusChanged: if (activeFocus) emailField.forceActiveFocus() + + FormCard.FormTextFieldDelegate { + id: emailField + label: i18n("Add an e-mail address:") + placeholderText: "user@example.com" + onTextChanged: Registration.email = text + Keys.onReturnPressed: { + if (root.nextAction.enabled) { + root.nextAction.trigger() + } + } + } + + FormCard.FormTextDelegate { + id: confirmMessage + text: i18n("Confirm e-mail address") + visible: false + description: i18n("A confirmation e-mail has been sent to your address. Please continue here after clicking on the confirmation link in the e-mail") + } + + FormCard.FormButtonDelegate { + id: resendButton + text: i18nc("@button", "Re-send confirmation e-mail") + onClicked: Registration.registerEmail() + visible: false + } + + nextAction: Kirigami.Action { + enabled: emailField.text.length > 0 + onTriggered: { + if (confirmMessage.visible) { + Registration.registerAccount() + } else { + Registration.registerEmail() + confirmMessage.visible = true + resendButton.visible = true + } + } + } + previousAction: Kirigami.Action { + onTriggered: root.processed("qrc:/Username.qml") + } +} diff --git a/src/qml/Component/Login/Homeserver.qml b/src/qml/Component/Login/Homeserver.qml index 5edb9a2a7..d6aafcc4a 100644 --- a/src/qml/Component/Login/Homeserver.qml +++ b/src/qml/Component/Login/Homeserver.qml @@ -1,4 +1,4 @@ -// SPDX-FileCopyrightText: 2020 Tobias Fella +// SPDX-FileCopyrightText: 2023 Tobias Fella // SPDX-License-Identifier: GPL-2.0-or-later import QtQuick 2.15 @@ -6,56 +6,42 @@ import QtQuick.Controls 2.15 as QQC2 import QtQuick.Layouts 1.15 import org.kde.kirigami 2.15 as Kirigami +import org.kde.kirigamiaddons.formcard 1.0 as FormCard import org.kde.neochat 1.0 LoginStep { id: root - readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText - property bool loading: false + onActiveFocusChanged: if (activeFocus) urlField.forceActiveFocus() - title: i18nc("@title", "Select a Homeserver") - - action: Kirigami.Action { - enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput - onTriggered: { - // TODO - console.log("register todo") + FormCard.FormTextFieldDelegate { + id: urlField + label: i18n("Server Url:") + validator: RegularExpressionValidator { + regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/ + } + onTextChanged: timer.restart() + statusMessage: Registration.status === Registration.ServerNoRegistration ? i18n("Registration is disabled on this server.") : "" + Keys.onReturnPressed: { + if (root.nextAction.enabled) { + root.nextAction.trigger() + } } } - onHomeserverChanged: { - LoginHelper.testHomeserver("@user:" + homeserver) + Timer { + id: timer + interval: 500 + onTriggered: Registration.homeserver = urlField.text } - Kirigami.FormLayout { - Component.onCompleted: Controller.testHomeserver(homeserver) - - QQC2.ComboBox { - id: serverCombo - - Kirigami.FormData.label: i18n("Homeserver:") - model: ["matrix.org", "kde.org", "tchncs.de", i18n("Other...")] - } - - QQC2.TextField { - id: customHomeserver - - Kirigami.FormData.label: i18n("Url:") - visible: serverCombo.currentIndex === 3 - onTextChanged: { - Controller.testHomeserver(text) - } - validator: RegularExpressionValidator { - regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/ - } - } - - QQC2.Button { - id: continueButton - text: i18nc("@action:button", "Continue") - action: root.action - } + nextAction: Kirigami.Action { + text: Registration.testing ? i18n("Loading") : null + enabled: Registration.status > Registration.ServerNoRegistration + onTriggered: root.processed("qrc:/Username.qml"); + } + previousAction: Kirigami.Action { + onTriggered: root.processed("qrc:/LoginRegister.qml") } } diff --git a/src/qml/Component/Login/Login.qml b/src/qml/Component/Login/Login.qml index 844842a5a..0449430f2 100644 --- a/src/qml/Component/Login/Login.qml +++ b/src/qml/Component/Login/Login.qml @@ -47,10 +47,9 @@ LoginStep { } enabled: LoginHelper.homeserverReachable } - // TODO: enable once we have registration - // previousAction: Kirigami.Action { - // onTriggered: { - // root.processed("qrc:/Login.qml") - // } - // } + previousAction: Kirigami.Action { + onTriggered: { + root.processed("qrc:/LoginRegister.qml") + } + } } diff --git a/src/qml/Component/Login/LoginRegister.qml b/src/qml/Component/Login/LoginRegister.qml index f2af06fdc..76c52b39a 100644 --- a/src/qml/Component/Login/LoginRegister.qml +++ b/src/qml/Component/Login/LoginRegister.qml @@ -13,9 +13,12 @@ import org.kde.neochat 1.0 LoginStep { id: root + onActiveFocusChanged: if (activeFocus) loginButton.forceActiveFocus() + Layout.fillWidth: true FormCard.FormButtonDelegate { + id: loginButton text: i18nc("@action:button", "Login") onClicked: root.processed("qrc:/Login.qml") } diff --git a/src/qml/Component/Login/LoginStep.qml b/src/qml/Component/Login/LoginStep.qml index ad047ab2a..88dc069fb 100644 --- a/src/qml/Component/Login/LoginStep.qml +++ b/src/qml/Component/Login/LoginStep.qml @@ -26,6 +26,6 @@ ColumnLayout { /// Show a message in a banner at the top of the page. signal showMessage(string message) + /// Clears any error messages currently being shown signal clearError() - } diff --git a/src/qml/Component/Login/RegisterPassword.qml b/src/qml/Component/Login/RegisterPassword.qml new file mode 100644 index 000000000..1ef139eb9 --- /dev/null +++ b/src/qml/Component/Login/RegisterPassword.qml @@ -0,0 +1,51 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 + +import org.kde.kirigami 2.12 as Kirigami +import org.kde.kirigamiaddons.formcard 1.0 as FormCard + +import org.kde.neochat 1.0 + +LoginStep { + id: root + + onActiveFocusChanged: if (activeFocus) passwordField.forceActiveFocus() + + FormCard.FormTextFieldDelegate { + id: passwordField + label: i18n("Password:") + echoMode: TextInput.Password + onTextChanged: Registration.password = text + Keys.onReturnPressed: { + confirmPasswordField.forceActiveFocus() + } + } + + FormCard.FormTextFieldDelegate { + id: confirmPasswordField + label: i18n("Confirm Password:") + enabled: passwordField.enabled + echoMode: TextInput.Password + statusMessage: passwordField.text.length === confirmPasswordField.text.length && passwordField.text !== confirmPasswordField.text ? i18n("The passwords do not match.") : "" + Keys.onReturnPressed: { + if (root.nextAction.enabled) { + root.nextAction.trigger() + } + } + } + + nextAction: Kirigami.Action { + onTriggered: { + passwordField.enabled = false + Registration.registerAccount() + } + enabled: passwordField.text === confirmPasswordField.text + } + + previousAction: Kirigami.Action { + onTriggered: root.processed("qrc:/Username.qml") + } +} diff --git a/src/qml/Component/Login/Terms.qml b/src/qml/Component/Login/Terms.qml new file mode 100644 index 000000000..ca4638cef --- /dev/null +++ b/src/qml/Component/Login/Terms.qml @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 +import QtQuick.Layouts 1.12 + +import org.kde.kirigami 2.12 as Kirigami +import org.kde.kirigamiaddons.formcard 1.0 as FormCard + +import org.kde.neochat 1.0 + +LoginStep { + id: root + + noControls: true + + FormCard.FormTextDelegate { + text: i18n("Terms & Conditions") + description: i18n("By continuing with the registration, you agree to the following terms and conditions:") + } + + Repeater { + model: Registration.terms + delegate: FormCard.FormTextDelegate { + text: "" + modelData.title + "" + onLinkActivated: Qt.openUrlExternally(modelData.url) + } + } + + nextAction: Kirigami.Action { + onTriggered: { + Registration.registerAccount() + } + } + previousAction: Kirigami.Action { + onTriggered: root.processed("qrc:/Username.qml") + } +} diff --git a/src/qml/Component/Login/Username.qml b/src/qml/Component/Login/Username.qml new file mode 100644 index 000000000..9b6ee75bd --- /dev/null +++ b/src/qml/Component/Login/Username.qml @@ -0,0 +1,46 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +import QtQuick 2.15 +import QtQuick.Controls 2.12 as QQC2 + +import org.kde.kirigami 2.12 as Kirigami +import org.kde.kirigamiaddons.formcard 1.0 as FormCard + +import org.kde.neochat 1.0 + +LoginStep { + id: root + + onActiveFocusChanged: if (activeFocus) usernameField.forceActiveFocus() + + FormCard.FormTextFieldDelegate { + id: usernameField + label: i18n("Username:") + placeholderText: "user" + onTextChanged: timer.restart() + statusMessage: Registration.status === Registration.UsernameTaken ? i18n("Username unavailable") : "" + Keys.onReturnPressed: { + if (root.nextAction.enabled) { + root.nextAction.trigger() + } + } + } + + Timer { + id: timer + interval: 500 + onTriggered: Registration.username = usernameField.text + } + + nextAction: Kirigami.Action { + text: Registration.status === Registration.TestingUsername ? i18n("Loading") : null + + onTriggered: root.processed("qrc:/RegisterPassword.qml") + enabled: Registration.status === Registration.Ready + } + + previousAction: Kirigami.Action { + onTriggered: root.processed("qrc:/Homeserver.qml") + } +} diff --git a/src/qml/Page/WelcomePage.qml b/src/qml/Page/WelcomePage.qml index 79cfb3317..cd8579321 100644 --- a/src/qml/Page/WelcomePage.qml +++ b/src/qml/Page/WelcomePage.qml @@ -41,7 +41,7 @@ FormCard.FormCardPage { FormCard.FormTextDelegate { id: welcomeMessage - text: AccountRegistry.accountCount > 0 ? i18n("Log in to a different account.") : i18n("Welcome to NeoChat! Continue by logging in.") + text: AccountRegistry.accountCount > 0 ? i18n("Log in to a different account or create a new account.") : i18n("Welcome to NeoChat! Continue by logging in or creating a new account.") } FormCard.FormDelegateSeparator { @@ -51,9 +51,10 @@ FormCard.FormCardPage { Loader { id: module Layout.fillWidth: true - source: "qrc:/Login.qml" + source: "qrc:/LoginRegister.qml" Connections { + id: stepConnections target: currentStep function onProcessed(nextUrl) { @@ -76,6 +77,23 @@ FormCard.FormCardPage { headerMessage.visible = false; } } + Connections { + target: Registration + function onNextStepChanged() { + if (Registration.nextStep === "m.login.recaptcha") { + stepConnections.onProcessed("qrc:/Captcha.qml") + } + if (Registration.nextStep === "m.login.terms") { + stepConnections.onProcessed("qrc:/Terms.qml") + } + if (Registration.nextStep === "m.login.email.identity") { + stepConnections.onProcessed("qrc:/Email.qml") + } + if (Registration.nextStep === "loading") { + stepConnections.onProcessed("qrc:/Loading.qml") + } + } + } Connections { target: LoginHelper function onErrorOccured(message) { @@ -92,7 +110,7 @@ FormCard.FormCardPage { FormCard.FormButtonDelegate { id: continueButton - text: root.currentStep.nextAction ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue") + text: root.currentStep.nextAction && root.currentStep.nextAction.text ? root.currentStep.nextAction.text : i18nc("@action:button", "Continue") visible: root.currentStep.nextAction onClicked: root.currentStep.nextAction.trigger() icon.name: "arrow-right" @@ -111,5 +129,8 @@ FormCard.FormCardPage { Component.onCompleted: { LoginHelper.init() module.item.forceActiveFocus() + Registration.username = "" + Registration.password = "" + Registration.email = "" } } diff --git a/src/registration.cpp b/src/registration.cpp new file mode 100644 index 000000000..344729256 --- /dev/null +++ b/src/registration.cpp @@ -0,0 +1,338 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "registration.h" + +#include +#include +#include + +#include +#include +#include + +#include "controller.h" + +#include + +using namespace Quotient; + +Registration::Registration() +{ + auto server = new QTcpServer(this); + server->listen(QHostAddress("127.0.0.1"_ls), 20847); + connect(server, &QTcpServer::newConnection, this, [=]() { + auto conn = server->nextPendingConnection(); + connect(conn, &QIODevice::readyRead, this, [=]() { + auto code = + "HTTP/1.0 200\nContent-type: text/html\n\n
"_ls + .arg(m_recaptchaSiteKey); + conn->write(code.toLatin1().data(), code.length()); + conn->close(); + }); + }); + + connect(this, &Registration::homeserverChanged, this, &Registration::testHomeserver); + connect(this, &Registration::usernameChanged, this, &Registration::testUsername); +} + +void Registration::setRecaptchaResponse(const QString &recaptchaResponse) +{ + m_recaptchaResponse = recaptchaResponse; + Q_EMIT recaptchaResponseChanged(); + registerAccount(); +} + +QString Registration::recaptchaResponse() const +{ + return m_recaptchaResponse; +} + +void Registration::setRecaptchaSiteKey(const QString &recaptchaSiteKey) +{ + m_recaptchaSiteKey = recaptchaSiteKey; + Q_EMIT recaptchaSiteKeyChanged(); +} + +QString Registration::recaptchaSiteKey() const +{ + return m_recaptchaSiteKey; +} + +void Registration::registerAccount() +{ + setStatus(Working); + Omittable authData = none; + if (nextStep() == "m.login.recaptcha"_ls) { + authData = QJsonObject{ + {"type"_ls, "m.login.recaptcha"_ls}, + {"response"_ls, m_recaptchaResponse}, + {"session"_ls, m_session}, + }; + } else if (nextStep() == "m.login.terms"_ls) { + authData = QJsonObject{ + {"type"_ls, "m.login.terms"_ls}, + {"session"_ls, m_session}, + }; + } else if (nextStep() == "m.login.email.identity"_ls) { + authData = QJsonObject{ + {"type"_ls, "m.login.email.identity"_ls}, + {"threepid_creds"_ls, + QJsonObject{ + {"sid"_ls, m_sid}, + {"client_secret"_ls, m_emailSecret}, + }}, + {"session"_ls, m_session}, + }; + } + auto job = m_connection->callApi("user"_ls, authData, m_username, m_password, QString(), QString(), true); + connect(job, &BaseJob::result, this, [=]() { + if (job->status() == BaseJob::Success) { + setNextStep("loading"_ls); + auto connection = new Connection(this); + auto matrixId = "@%1:%2"_ls.arg(m_username, m_homeserver); + connection->resolveServer(matrixId); + + auto displayName = "NeoChat %1 %2 %3 %4"_ls.arg(QSysInfo::machineHostName(), + QSysInfo::productType(), + QSysInfo::productVersion(), + QSysInfo::currentCpuArchitecture()); + connection->loginWithPassword(matrixId, m_password, displayName); + + connect(connection, &Connection::connected, this, [this, displayName, connection] { + AccountSettings account(connection->userId()); + account.setKeepLoggedIn(true); + account.setHomeserver(connection->homeserver()); + account.setDeviceId(connection->deviceId()); + account.setDeviceName(displayName); + if (!Controller::instance().saveAccessTokenToKeyChain(account, connection->accessToken())) { + qWarning() << "Couldn't save access token"; + } + account.sync(); + Controller::instance().addConnection(connection); + Controller::instance().setActiveConnection(connection); + connectSingleShot(connection, &Connection::syncDone, this, []() { + Q_EMIT Controller::instance().initiated(); + }); + m_connection = nullptr; + }); + + return; + } + const auto &data = job->jsonData(); + m_session = data["session"_ls].toString(); + const auto ¶ms = data["params"_ls].toObject(); + + // I'm not motivated enough to figure out how we should handle the flow stuff, so: + // If there is a flow that requires e-mail, we use that, to make sure that the user can recover the account from a forgotten password. + // Otherwise, we're using the first flow. + auto selectedFlow = data["flows"_ls].toArray()[0].toObject()["stages"_ls].toArray(); + for (const auto &flow : data["flows"_ls].toArray()) { + if (flow.toObject()["stages"_ls].toArray().contains("m.login.email.identity"_ls)) { + selectedFlow = flow.toObject()["stages"_ls].toArray(); + } + } + + setNextStep(selectedFlow[data["completed"_ls].toArray().size()].toString()); + m_recaptchaSiteKey = params["m.login.recaptcha"_ls]["public_key"_ls].toString(); + Q_EMIT recaptchaSiteKeyChanged(); + m_terms.clear(); + for (const auto &term : params["m.login.terms"_ls]["policies"_ls].toObject().keys()) { + QVariantMap termData; + termData["title"_ls] = params["m.login.terms"_ls]["policies"_ls][term]["en"_ls]["name"_ls].toString(); + termData["url"_ls] = params["m.login.terms"_ls]["policies"_ls][term]["en"_ls]["url"_ls].toString(); + m_terms += termData; + Q_EMIT termsChanged(); + } + }); +} + +QString Registration::homeserver() const +{ + return m_homeserver; +} + +void Registration::setHomeserver(const QString &url) +{ + m_homeserver = url; + Q_EMIT homeserverChanged(); +} + +void Registration::testHomeserver() +{ + if (m_homeserver.isEmpty()) { + setStatus(NoServer); + return; + } + setStatus(TestingHomeserver); + if (m_connection) { + delete m_connection; + } + + m_connection = new Connection(this); + m_connection->resolveServer("@user:%1"_ls.arg(m_homeserver)); + connectSingleShot(m_connection.data(), &Connection::loginFlowsChanged, this, [this]() { + if (m_testServerJob) { + delete m_testServerJob; + } + m_testServerJob = m_connection->callApi("user"_ls, none, "user"_ls, QString(), QString(), QString(), false); + connect(m_testServerJob.data(), &BaseJob::finished, this, [this]() { + if (m_testServerJob->error() == BaseJob::StatusCode::ContentAccessError) { + setStatus(ServerNoRegistration); + return; + } + if (m_testServerJob->status().code != 106) { + setStatus(InvalidServer); + return; + } + if (!m_username.isEmpty()) { + setStatus(TestingUsername); + testUsername(); + } else { + setStatus(NoUsername); + } + }); + }); +} + +void Registration::setUsername(const QString &username) +{ + m_username = username; + Q_EMIT usernameChanged(); +} + +QString Registration::username() const +{ + return m_username; +} + +void Registration::testUsername() +{ + if (status() <= ServerNoRegistration) { + return; + } + setStatus(TestingUsername); + if (m_usernameJob) { + m_usernameJob->abandon(); + } + if (m_username.isEmpty()) { + setStatus(NoUsername); + return; + } + + m_usernameJob = m_connection->callApi(m_username); + connect(m_usernameJob, &BaseJob::result, this, [this]() { + setStatus(m_usernameJob->error() == BaseJob::StatusCode::Success && *m_usernameJob->available() ? Ready : UsernameTaken); + }); +} + +QVector Registration::terms() const +{ + return m_terms; +} + +QString Registration::password() const +{ + return m_password; +} + +void Registration::setPassword(const QString &password) +{ + m_password = password; + Q_EMIT passwordChanged(); +} + +NeoChatRegisterJob::NeoChatRegisterJob(const QString &kind, + const Omittable &auth, + const QString &username, + const QString &password, + const QString &deviceId, + const QString &initialDeviceDisplayName, + Omittable inhibitLogin) + : BaseJob(HttpVerb::Post, "RegisterJob"_ls, QByteArrayLiteral("/_matrix/client/r0/register"), false) +{ + QJsonObject _data; + if (auth) { + addParam<>(_data, "auth"_ls, auth); + } + addParam<>(_data, "username"_ls, username); + addParam(_data, "password"_ls, password); + addParam(_data, "device_id"_ls, deviceId); + addParam(_data, "initial_device_display_name"_ls, initialDeviceDisplayName); + addParam(_data, "inhibit_login"_ls, inhibitLogin); + addParam(_data, "kind"_ls, kind); + addParam(_data, "refresh_token"_ls, false); + setRequestData(_data); +} + +QString Registration::email() const +{ + return m_email; +} + +void Registration::setEmail(const QString &email) +{ + m_email = email; + Q_EMIT emailChanged(); +} + +QString Registration::nextStep() const +{ + return m_nextStep; +} + +void Registration::setNextStep(const QString &nextStep) +{ + m_nextStep = nextStep; + Q_EMIT nextStepChanged(); +} + +Registration::Status Registration::status() const +{ + return m_status; +} + +QString Registration::statusString() const +{ + switch (m_status) { + case NoServer: + return i18n("No server."); + case TestingHomeserver: + return i18n("Checking Server availability."); + case InvalidServer: + return i18n("This is not a valid server."); + case ServerNoRegistration: + return i18n("Regisration for this server is disabled."); + case NoUsername: + return i18n("No username."); + case TestingUsername: + return i18n("Checking username availability."); + case UsernameTaken: + return i18n("This username is not available."); + case Ready: + return i18n("Continue"); + case Working: + return i18n("Working"); + } + return {}; +} + +void Registration::setStatus(Status status) +{ + m_status = status; + Q_EMIT statusChanged(); +} + +void Registration::registerEmail() +{ + m_emailSecret = QString::fromLatin1(QUuid::createUuid().toString().toLatin1().toBase64()); + EmailValidationData data; + data.email = m_email; + data.clientSecret = m_emailSecret; + data.sendAttempt = 0; + + auto job = m_connection->callApi(data); + connect(job, &BaseJob::finished, this, [=]() { + m_sid = job->jsonData()["sid"_ls].toString(); + }); +} diff --git a/src/registration.h b/src/registration.h new file mode 100644 index 000000000..1c8ae571d --- /dev/null +++ b/src/registration.h @@ -0,0 +1,163 @@ +// SPDX-FileCopyrightText: 2023 Tobias Fella +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +#include +#include + +#include + +#include +#include + +namespace Quotient +{ +class Connection; +class CheckUsernameAvailabilityJob; +} + +class NeoChatRegisterJob : public Quotient::BaseJob +{ +public: + explicit NeoChatRegisterJob(const QString &kind = QStringLiteral("user"), + const Quotient::Omittable &auth = Quotient::none, + const QString &username = {}, + const QString &password = {}, + const QString &deviceId = {}, + const QString &initialDeviceDisplayName = {}, + Quotient::Omittable inhibitLogin = Quotient::none); + + QString userId() const + { + return loadFromJson(QStringLiteral("user_id")); + } + + QString accessToken() const + { + return loadFromJson(QStringLiteral("access_token")); + } + + QString homeServer() const + { + return loadFromJson(QStringLiteral("home_server")); + } + + QString deviceId() const + { + return loadFromJson(QStringLiteral("device_id")); + } +}; + +class Registration : public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString homeserver READ homeserver WRITE setHomeserver NOTIFY homeserverChanged) + Q_PROPERTY(QString username READ username WRITE setUsername NOTIFY usernameChanged) + Q_PROPERTY(QString recaptchaSiteKey READ recaptchaSiteKey WRITE setRecaptchaSiteKey NOTIFY recaptchaSiteKeyChanged) + Q_PROPERTY(QString recaptchaResponse READ recaptchaResponse WRITE setRecaptchaResponse NOTIFY recaptchaResponseChanged) + Q_PROPERTY(QString password READ password WRITE setPassword NOTIFY passwordChanged) + Q_PROPERTY(QString email READ email WRITE setEmail NOTIFY emailChanged) + Q_PROPERTY(QString nextStep READ nextStep WRITE setNextStep NOTIFY nextStepChanged) + Q_PROPERTY(QVector terms READ terms NOTIFY termsChanged) + Q_PROPERTY(Status status READ status NOTIFY statusChanged) + Q_PROPERTY(QString statusString READ statusString NOTIFY statusChanged) + +public: + enum Status { + NoServer, + TestingHomeserver, + InvalidServer, + ServerNoRegistration, + NoUsername, + TestingUsername, + UsernameTaken, + Ready, + Working, + }; + Q_ENUM(Status); + static Registration &instance() + { + static Registration _instance; + return _instance; + } + + Q_INVOKABLE void registerAccount(); + Q_INVOKABLE void registerEmail(); + + void setRecaptchaSiteKey(const QString &recaptchaSiteKey); + QString recaptchaSiteKey() const; + + void setRecaptchaResponse(const QString &response); + QString recaptchaResponse() const; + + void setHomeserver(const QString &url); + QString homeserver() const; + + QString username() const; + void setUsername(const QString &username); + + QString password() const; + void setPassword(const QString &password); + + [[nodiscard]] QString email() const; + void setEmail(const QString &email); + + QString nextStep() const; + void setNextStep(const QString &nextStep); + + QVector terms() const; + + Status status() const; + QString statusString() const; + +Q_SIGNALS: + void recaptchaSiteKeyChanged(); + void recaptchaResponseChanged(); + void homeserverChanged(); + void homeserverAvailableChanged(); + void testingChanged(); + void usernameChanged(); + void usernameAvailableChanged(); + void testingUsernameChanged(); + void flowsChanged(); + void termsChanged(); + void passwordChanged(); + void emailChanged(); + void nextStepChanged(); + void statusChanged(); + +private: + QString m_recaptchaSiteKey; + QString m_recaptchaResponse; + QString m_homeserver; + QString m_username; + QString m_password; + QVector m_terms; + QString m_email; + Status m_status = NoServer; + QString m_nextStep; + QString m_session; + QString m_sid; + QString m_emailSecret; + + QPointer m_usernameJob; + QPointer m_testServerJob; + QVector> m_flows; + QPointer m_connection; + + void testHomeserver(); + void testUsername(); + QCoro::Task loadFlows(); + void setStatus(Status status); + + Registration(); +}; diff --git a/src/res.qrc b/src/res.qrc index 1d2d8a0b5..dcb06a8d1 100644 --- a/src/res.qrc +++ b/src/res.qrc @@ -65,10 +65,15 @@ qml/Component/Timeline/AvatarFlow.qml qml/Component/Login/LoginStep.qml qml/Component/Login/Login.qml + qml/Component/Login/Homeserver.qml + qml/Component/Login/Username.qml + qml/Component/Login/RegisterPassword.qml + qml/Component/Login/Captcha.qml + qml/Component/Login/Terms.qml + qml/Component/Login/Email.qml qml/Component/Login/Password.qml qml/Component/Login/LoginRegister.qml qml/Component/Login/Loading.qml - qml/Component/Login/Homeserver.qml qml/Component/Login/LoginMethod.qml qml/Component/Login/Sso.qml qml/Dialog/UserDetailDialog.qml