Implement account registration

Implements #7
This commit is contained in:
Tobias Fella
2023-08-28 10:05:09 +00:00
parent cc60dde62d
commit 59164d3bb2
17 changed files with 829 additions and 53 deletions

View File

@@ -77,7 +77,7 @@ ecm_setup_version(${PROJECT_VERSION}
VERSION_HEADER ${CMAKE_CURRENT_BINARY_DIR}/neochat-version.h 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 set_package_properties(Qt${QT_MAJOR_VERSION} PROPERTIES
TYPE REQUIRED TYPE REQUIRED
PURPOSE "Basic application components" PURPOSE "Basic application components"

View File

@@ -9,7 +9,7 @@
android:versionName="${versionName}" android:versionName="${versionName}"
android:versionCode="${versionCode}" android:versionCode="${versionCode}"
android:installLocation="auto"> android:installLocation="auto">
<application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat"> <application android:name="org.qtproject.qt5.android.bindings.QtApplication" android:label="NeoChat" android:icon="@drawable/neochat" android:usesCleartextTraffic="true">
<activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation" <activity android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation"
android:name="org.qtproject.qt5.android.bindings.QtActivity" android:name="org.qtproject.qt5.android.bindings.QtActivity"
android:label="NeoChat" android:label="NeoChat"

View File

@@ -125,6 +125,7 @@ add_library(neochat STATIC
events/pollevent.cpp events/pollevent.cpp
pollhandler.cpp pollhandler.cpp
utils.h utils.h
registration.cpp
) )
ecm_qt_declare_logging_category(neochat ecm_qt_declare_logging_category(neochat
@@ -141,6 +142,11 @@ add_executable(neochat-app
${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc ${CMAKE_CURRENT_SOURCE_DIR}/res.generated.qrc
) )
if(TARGET Qt::WebView)
target_link_libraries(neochat-app PUBLIC Qt::WebView)
target_compile_definitions(neochat-app PUBLIC -DHAVE_WEBVIEW)
endif()
target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR}) target_include_directories(neochat-app PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat-app PRIVATE target_link_libraries(neochat-app PRIVATE
@@ -177,6 +183,7 @@ endif()
target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR}) target_include_directories(neochat PRIVATE ${CMAKE_BINARY_DIR})
target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core) target_link_libraries(neochat PUBLIC Qt::Core Qt::Quick Qt::Qml Qt::Gui Qt::Multimedia Qt::Network Qt::QuickControls2 KF${QT_MAJOR_VERSION}::I18n KF${QT_MAJOR_VERSION}::Kirigami2 KF${QT_MAJOR_VERSION}::Notifications KF${QT_MAJOR_VERSION}::ConfigCore KF${QT_MAJOR_VERSION}::ConfigGui KF${QT_MAJOR_VERSION}::CoreAddons KF${QT_MAJOR_VERSION}::SonnetCore KF${QT_MAJOR_VERSION}::ItemModels Quotient${QUOTIENT_SUFFIX} cmark::cmark QCoro::Core)
kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc) kconfig_add_kcfg_files(neochat GENERATE_MOC neochatconfig.kcfgc)
if(NEOCHAT_FLATPAK) if(NEOCHAT_FLATPAK)

View File

@@ -18,6 +18,10 @@
#include <QApplication> #include <QApplication>
#endif #endif
#ifdef HAVE_WEBVIEW
#include <QtWebView>
#endif
#include <KAboutData> #include <KAboutData>
#ifdef HAVE_KDBUSADDONS #ifdef HAVE_KDBUSADDONS
#include <KDBusService> #include <KDBusService>
@@ -95,6 +99,7 @@
#include "runner.h" #include "runner.h"
#include <QDBusConnection> #include <QDBusConnection>
#endif #endif
#include "registration.h"
#ifdef Q_OS_WINDOWS #ifdef Q_OS_WINDOWS
#include <Windows.h> #include <Windows.h>
@@ -139,6 +144,10 @@ int main(int argc, char *argv[])
QNetworkProxyFactory::setUseSystemConfiguration(true); QNetworkProxyFactory::setUseSystemConfiguration(true);
#ifdef HAVE_WEBVIEW
QtWebView::initialize();
#endif
#ifdef Q_OS_ANDROID #ifdef Q_OS_ANDROID
QGuiApplication app(argc, argv); QGuiApplication app(argc, argv);
QQuickStyle::setStyle(QStringLiteral("org.kde.breeze")); 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, "AccountRegistry", &Controller::instance().accounts());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "SpaceHierarchyCache", &SpaceHierarchyCache::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance()); qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "CustomEmojiModel", &CustomEmojiModel::instance());
qmlRegisterSingletonInstance("org.kde.neochat", 1, 0, "Registration", &Registration::instance());
qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler"); qmlRegisterType<ActionsHandler>("org.kde.neochat", 1, 0, "ActionsHandler");
qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler"); qmlRegisterType<ChatDocumentHandler>("org.kde.neochat", 1, 0, "ChatDocumentHandler");
qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel"); qmlRegisterType<RoomListModel>("org.kde.neochat", 1, 0, "RoomListModel");

View File

@@ -0,0 +1,49 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// 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")
}
}

View File

@@ -0,0 +1,59 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// 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 <b>after</b> 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")
}
}

View File

@@ -1,4 +1,4 @@
// SPDX-FileCopyrightText: 2020 Tobias Fella <tobias.fella@kde.org> // SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
import QtQuick 2.15 import QtQuick 2.15
@@ -6,56 +6,42 @@ import QtQuick.Controls 2.15 as QQC2
import QtQuick.Layouts 1.15 import QtQuick.Layouts 1.15
import org.kde.kirigami 2.15 as Kirigami import org.kde.kirigami 2.15 as Kirigami
import org.kde.kirigamiaddons.formcard 1.0 as FormCard
import org.kde.neochat 1.0 import org.kde.neochat 1.0
LoginStep { LoginStep {
id: root id: root
readonly property var homeserver: customHomeserver.visible ? customHomeserver.text : serverCombo.currentText onActiveFocusChanged: if (activeFocus) urlField.forceActiveFocus()
property bool loading: false
title: i18nc("@title", "Select a Homeserver") FormCard.FormTextFieldDelegate {
id: urlField
action: Kirigami.Action { label: i18n("Server Url:")
enabled: LoginHelper.homeserverReachable && !customHomeserver.visible || customHomeserver.acceptableInput validator: RegularExpressionValidator {
onTriggered: { regularExpression: /([a-zA-Z0-9\-]+\.)*[a-zA-Z0-9]+(:[0-9]+)?/
// TODO }
console.log("register todo") 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: { Timer {
LoginHelper.testHomeserver("@user:" + homeserver) id: timer
interval: 500
onTriggered: Registration.homeserver = urlField.text
} }
Kirigami.FormLayout { nextAction: Kirigami.Action {
Component.onCompleted: Controller.testHomeserver(homeserver) text: Registration.testing ? i18n("Loading") : null
enabled: Registration.status > Registration.ServerNoRegistration
QQC2.ComboBox { onTriggered: root.processed("qrc:/Username.qml");
id: serverCombo }
previousAction: Kirigami.Action {
Kirigami.FormData.label: i18n("Homeserver:") onTriggered: root.processed("qrc:/LoginRegister.qml")
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
}
} }
} }

View File

@@ -47,10 +47,9 @@ LoginStep {
} }
enabled: LoginHelper.homeserverReachable enabled: LoginHelper.homeserverReachable
} }
// TODO: enable once we have registration previousAction: Kirigami.Action {
// previousAction: Kirigami.Action { onTriggered: {
// onTriggered: { root.processed("qrc:/LoginRegister.qml")
// root.processed("qrc:/Login.qml") }
// } }
// }
} }

View File

@@ -13,9 +13,12 @@ import org.kde.neochat 1.0
LoginStep { LoginStep {
id: root id: root
onActiveFocusChanged: if (activeFocus) loginButton.forceActiveFocus()
Layout.fillWidth: true Layout.fillWidth: true
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
id: loginButton
text: i18nc("@action:button", "Login") text: i18nc("@action:button", "Login")
onClicked: root.processed("qrc:/Login.qml") onClicked: root.processed("qrc:/Login.qml")
} }

View File

@@ -26,6 +26,6 @@ ColumnLayout {
/// Show a message in a banner at the top of the page. /// Show a message in a banner at the top of the page.
signal showMessage(string message) signal showMessage(string message)
/// Clears any error messages currently being shown
signal clearError() signal clearError()
} }

View File

@@ -0,0 +1,51 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// 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")
}
}

View File

@@ -0,0 +1,39 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// 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: "<a href=\"" + modelData.url + "\">" + modelData.title + "</a>"
onLinkActivated: Qt.openUrlExternally(modelData.url)
}
}
nextAction: Kirigami.Action {
onTriggered: {
Registration.registerAccount()
}
}
previousAction: Kirigami.Action {
onTriggered: root.processed("qrc:/Username.qml")
}
}

View File

@@ -0,0 +1,46 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// 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")
}
}

View File

@@ -41,7 +41,7 @@ FormCard.FormCardPage {
FormCard.FormTextDelegate { FormCard.FormTextDelegate {
id: welcomeMessage 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 { FormCard.FormDelegateSeparator {
@@ -51,9 +51,10 @@ FormCard.FormCardPage {
Loader { Loader {
id: module id: module
Layout.fillWidth: true Layout.fillWidth: true
source: "qrc:/Login.qml" source: "qrc:/LoginRegister.qml"
Connections { Connections {
id: stepConnections
target: currentStep target: currentStep
function onProcessed(nextUrl) { function onProcessed(nextUrl) {
@@ -76,6 +77,23 @@ FormCard.FormCardPage {
headerMessage.visible = false; 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 { Connections {
target: LoginHelper target: LoginHelper
function onErrorOccured(message) { function onErrorOccured(message) {
@@ -92,7 +110,7 @@ FormCard.FormCardPage {
FormCard.FormButtonDelegate { FormCard.FormButtonDelegate {
id: continueButton 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 visible: root.currentStep.nextAction
onClicked: root.currentStep.nextAction.trigger() onClicked: root.currentStep.nextAction.trigger()
icon.name: "arrow-right" icon.name: "arrow-right"
@@ -111,5 +129,8 @@ FormCard.FormCardPage {
Component.onCompleted: { Component.onCompleted: {
LoginHelper.init() LoginHelper.init()
module.item.forceActiveFocus() module.item.forceActiveFocus()
Registration.username = ""
Registration.password = ""
Registration.email = ""
} }
} }

338
src/registration.cpp Normal file
View File

@@ -0,0 +1,338 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#include "registration.h"
#include <QTcpServer>
#include <QTcpSocket>
#include <QThread>
#include <Quotient/csapi/registration.h>
#include <Quotient/qt_connection_util.h>
#include <Quotient/settings.h>
#include "controller.h"
#include <KLocalizedString>
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<html><head><script src=\"https://www.google.com/recaptcha/api.js\" async defer></script></head><body style=\"background: #00000000\"><center><div class=\"g-recaptcha\" data-sitekey=\"%1\"></div></center></body></html>"_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<QJsonObject> 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<NeoChatRegisterJob>("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 &params = 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<NeoChatRegisterJob>("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<CheckUsernameAvailabilityJob>(m_username);
connect(m_usernameJob, &BaseJob::result, this, [this]() {
setStatus(m_usernameJob->error() == BaseJob::StatusCode::Success && *m_usernameJob->available() ? Ready : UsernameTaken);
});
}
QVector<QVariantMap> 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<QJsonObject> &auth,
const QString &username,
const QString &password,
const QString &deviceId,
const QString &initialDeviceDisplayName,
Omittable<bool> 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<IfNotEmpty>(_data, "password"_ls, password);
addParam<IfNotEmpty>(_data, "device_id"_ls, deviceId);
addParam<IfNotEmpty>(_data, "initial_device_display_name"_ls, initialDeviceDisplayName);
addParam<IfNotEmpty>(_data, "inhibit_login"_ls, inhibitLogin);
addParam<IfNotEmpty>(_data, "kind"_ls, kind);
addParam<IfNotEmpty>(_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<RequestTokenToRegisterEmailJob>(data);
connect(job, &BaseJob::finished, this, [=]() {
m_sid = job->jsonData()["sid"_ls].toString();
});
}

163
src/registration.h Normal file
View File

@@ -0,0 +1,163 @@
// SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
// SPDX-License-Identifier: GPL-2.0-or-later
#pragma once
#include <QJsonObject>
#include <QObject>
#include <QPointer>
#include <QString>
#include <QVariantMap>
#include <QVector>
#include <Quotient/csapi/registration.h>
#include <Task>
#include <Quotient/connection.h>
#include <Quotient/jobs/basejob.h>
#include <Quotient/util.h>
namespace Quotient
{
class Connection;
class CheckUsernameAvailabilityJob;
}
class NeoChatRegisterJob : public Quotient::BaseJob
{
public:
explicit NeoChatRegisterJob(const QString &kind = QStringLiteral("user"),
const Quotient::Omittable<QJsonObject> &auth = Quotient::none,
const QString &username = {},
const QString &password = {},
const QString &deviceId = {},
const QString &initialDeviceDisplayName = {},
Quotient::Omittable<bool> inhibitLogin = Quotient::none);
QString userId() const
{
return loadFromJson<QString>(QStringLiteral("user_id"));
}
QString accessToken() const
{
return loadFromJson<QString>(QStringLiteral("access_token"));
}
QString homeServer() const
{
return loadFromJson<QString>(QStringLiteral("home_server"));
}
QString deviceId() const
{
return loadFromJson<QString>(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<QVariantMap> 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<QVariantMap> 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<QVariantMap> m_terms;
QString m_email;
Status m_status = NoServer;
QString m_nextStep;
QString m_session;
QString m_sid;
QString m_emailSecret;
QPointer<Quotient::CheckUsernameAvailabilityJob> m_usernameJob;
QPointer<NeoChatRegisterJob> m_testServerJob;
QVector<QVector<QString>> m_flows;
QPointer<Quotient::Connection> m_connection;
void testHomeserver();
void testUsername();
QCoro::Task<void> loadFlows();
void setStatus(Status status);
Registration();
};

View File

@@ -65,10 +65,15 @@
<file alias="AvatarFlow.qml">qml/Component/Timeline/AvatarFlow.qml</file> <file alias="AvatarFlow.qml">qml/Component/Timeline/AvatarFlow.qml</file>
<file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file> <file alias="LoginStep.qml">qml/Component/Login/LoginStep.qml</file>
<file alias="Login.qml">qml/Component/Login/Login.qml</file> <file alias="Login.qml">qml/Component/Login/Login.qml</file>
<file alias="Homeserver.qml">qml/Component/Login/Homeserver.qml</file>
<file alias="Username.qml">qml/Component/Login/Username.qml</file>
<file alias="RegisterPassword.qml">qml/Component/Login/RegisterPassword.qml</file>
<file alias="Captcha.qml">qml/Component/Login/Captcha.qml</file>
<file alias="Terms.qml">qml/Component/Login/Terms.qml</file>
<file alias="Email.qml">qml/Component/Login/Email.qml</file>
<file alias="Password.qml">qml/Component/Login/Password.qml</file> <file alias="Password.qml">qml/Component/Login/Password.qml</file>
<file alias="LoginRegister.qml">qml/Component/Login/LoginRegister.qml</file> <file alias="LoginRegister.qml">qml/Component/Login/LoginRegister.qml</file>
<file alias="Loading.qml">qml/Component/Login/Loading.qml</file> <file alias="Loading.qml">qml/Component/Login/Loading.qml</file>
<file alias="Homeserver.qml">qml/Component/Login/Homeserver.qml</file>
<file alias="LoginMethod.qml">qml/Component/Login/LoginMethod.qml</file> <file alias="LoginMethod.qml">qml/Component/Login/LoginMethod.qml</file>
<file alias="Sso.qml">qml/Component/Login/Sso.qml</file> <file alias="Sso.qml">qml/Component/Login/Sso.qml</file>
<file alias="UserDetailDialog.qml">qml/Dialog/UserDetailDialog.qml</file> <file alias="UserDetailDialog.qml">qml/Dialog/UserDetailDialog.qml</file>