From 4659d0efc274f2a955b203ca5b00cf1dfc26d5fc Mon Sep 17 00:00:00 2001 From: Konstantinos Sideris Date: Mon, 12 Mar 2018 22:23:26 +0200 Subject: [PATCH] Implement user registration with reCAPTCHA fixes #264 --- CMakeLists.txt | 3 +- README.md | 2 + cmake/MatrixStructs.cmake | 2 +- include/LoginPage.h | 2 +- include/MainWindow.h | 1 + include/MatrixClient.h | 7 ++- include/Register.h | 52 ----------------------- include/RegisterPage.h | 9 ++++ include/dialogs/ReCaptcha.hpp | 28 ++++++++++++ resources/styles/nheko-dark.qss | 1 + resources/styles/nheko.qss | 1 + src/LoginPage.cc | 2 +- src/MainWindow.cc | 12 +++++- src/MatrixClient.cc | 67 ++++++++++++++++++----------- src/Register.cc | 53 ----------------------- src/RegisterPage.cc | 30 +++++++++++++ src/dialogs/ReCaptcha.cpp | 75 +++++++++++++++++++++++++++++++++ 17 files changed, 211 insertions(+), 136 deletions(-) delete mode 100644 include/Register.h create mode 100644 include/dialogs/ReCaptcha.hpp delete mode 100644 src/Register.cc create mode 100644 src/dialogs/ReCaptcha.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 225ad7a1..48c79496 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -144,6 +144,7 @@ set(SRC_FILES src/dialogs/LeaveRoom.cc src/dialogs/Logout.cc src/dialogs/ReadReceipts.cc + src/dialogs/ReCaptcha.cpp # Emoji src/emoji/Category.cc @@ -192,7 +193,6 @@ set(SRC_FILES src/MainWindow.cc src/MatrixClient.cc src/QuickSwitcher.cc - src/Register.cc src/RegisterPage.cc src/RoomInfoListItem.cc src/RoomList.cc @@ -238,6 +238,7 @@ qt5_wrap_cpp(MOC_HEADERS include/dialogs/LeaveRoom.h include/dialogs/Logout.h include/dialogs/ReadReceipts.h + include/dialogs/ReCaptcha.hpp # Emoji include/emoji/Category.h diff --git a/README.md b/README.md index 1944a89c..d3c4519e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,7 @@ nheko [![Latest Release](https://img.shields.io/github/release/mujx/nheko.svg)](https://github.com/mujx/nheko/releases) [![Chat on Matrix](https://img.shields.io/badge/chat-on%20matrix-blue.svg)](https://matrix.to/#/#nheko:matrix.org) [![AUR: nheko-git](https://img.shields.io/badge/AUR-nheko--git-blue.svg)](https://aur.archlinux.org/packages/nheko-git) +[![AUR: nheko](https://img.shields.io/badge/AUR-nheko-blue.svg)](https://aur.archlinux.org/packages/nheko) The motivation behind the project is to provide a native desktop app for [Matrix] that feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IRC client. @@ -14,6 +15,7 @@ feels more like a mainstream chat app ([Riot], Telegram etc) and less like an IR Most of the features you would expect from a chat application are missing right now but we are getting close to a more feature complete client. Specifically there is support for: +- User registration. - Creating, joining & leaving rooms. - Sending & receiving invites. - Sending & receiving files and emoji (inline widgets for images, audio and file messages). diff --git a/cmake/MatrixStructs.cmake b/cmake/MatrixStructs.cmake index 42071b61..61cf619e 100644 --- a/cmake/MatrixStructs.cmake +++ b/cmake/MatrixStructs.cmake @@ -23,7 +23,7 @@ ExternalProject_Add( MatrixStructs GIT_REPOSITORY https://github.com/mujx/matrix-structs - GIT_TAG a1beea3b115f037e26c15f22ed911341b3893411 + GIT_TAG 850100c0ac2b5a04720b2a1f09270749bf99f7dd BUILD_IN_SOURCE 1 SOURCE_DIR ${MATRIX_STRUCTS_ROOT} diff --git a/include/LoginPage.h b/include/LoginPage.h index b2b40537..34835229 100644 --- a/include/LoginPage.h +++ b/include/LoginPage.h @@ -41,7 +41,7 @@ public: signals: void backButtonClicked(); void loggingIn(); - void errorOccured(); + void errorOccurred(); protected: void paintEvent(QPaintEvent *event) override; diff --git a/include/MainWindow.h b/include/MainWindow.h index e3a5e19d..d747f9b4 100644 --- a/include/MainWindow.h +++ b/include/MainWindow.h @@ -49,6 +49,7 @@ class InviteUsers; class JoinRoom; class LeaveRoom; class Logout; +class ReCaptcha; } class MainWindow : public QMainWindow diff --git a/include/MatrixClient.h b/include/MatrixClient.h index 46d946c7..69fa72bc 100644 --- a/include/MatrixClient.h +++ b/include/MatrixClient.h @@ -54,7 +54,8 @@ public: void login(const QString &username, const QString &password) noexcept; void registerUser(const QString &username, const QString &password, - const QString &server) noexcept; + const QString &server, + const QString &session = "") noexcept; void versions() noexcept; void fetchRoomAvatar(const QString &roomid, const QUrl &avatar_url); //! Download user's avatar. @@ -109,6 +110,10 @@ public slots: signals: void loginError(const QString &error); void registerError(const QString &error); + void registrationFlow(const QString &user, + const QString &pass, + const QString &server, + const QString &session); void versionError(const QString &error); void loggedOut(); diff --git a/include/Register.h b/include/Register.h deleted file mode 100644 index ed903172..00000000 --- a/include/Register.h +++ /dev/null @@ -1,52 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#pragma once - -#include "Deserializable.h" -#include - -class RegisterRequest -{ -public: - RegisterRequest(); - RegisterRequest(const QString &username, const QString &password); - - QByteArray serialize() noexcept; - - void setPassword(QString password) { password_ = password; }; - void setUser(QString username) { user_ = username; }; - -private: - QString user_; - QString password_; -}; - -class RegisterResponse : public Deserializable -{ -public: - void deserialize(const QJsonDocument &data) override; - - QString getAccessToken() { return access_token_; }; - QString getHomeServer() { return home_server_; }; - QString getUserId() { return user_id_; }; - -private: - QString access_token_; - QString home_server_; - QString user_id_; -}; diff --git a/include/RegisterPage.h b/include/RegisterPage.h index b98e59de..32f2fcf2 100644 --- a/include/RegisterPage.h +++ b/include/RegisterPage.h @@ -20,12 +20,17 @@ #include #include #include +#include class FlatButton; class MatrixClient; class RaisedButton; class TextField; +namespace dialogs { +class ReCaptcha; +} + class RegisterPage : public QWidget { Q_OBJECT @@ -38,6 +43,8 @@ protected: signals: void backButtonClicked(); + void errorOccurred(); + void registering(); private slots: void onBackButtonClicked(); @@ -70,4 +77,6 @@ private: // Matrix client API provider. QSharedPointer client_; + //! ReCaptcha dialog. + std::shared_ptr captchaDialog_; }; diff --git a/include/dialogs/ReCaptcha.hpp b/include/dialogs/ReCaptcha.hpp new file mode 100644 index 00000000..1eda40c7 --- /dev/null +++ b/include/dialogs/ReCaptcha.hpp @@ -0,0 +1,28 @@ +#pragma once + +#include + +class FlatButton; +class RaisedButton; + +namespace dialogs { + +class ReCaptcha : public QWidget +{ + Q_OBJECT + +public: + ReCaptcha(const QString &server, const QString &session, QWidget *parent = nullptr); + +protected: + void paintEvent(QPaintEvent *event) override; + +signals: + void closing(); + +private: + FlatButton *openCaptchaBtn_; + RaisedButton *confirmBtn_; + RaisedButton *cancelBtn_; +}; +} // dialogs diff --git a/resources/styles/nheko-dark.qss b/resources/styles/nheko-dark.qss index a9da770f..f70fd8e2 100644 --- a/resources/styles/nheko-dark.qss +++ b/resources/styles/nheko-dark.qss @@ -101,6 +101,7 @@ Avatar { } dialogs--Logout, +dialogs--ReCaptcha, dialogs--LeaveRoom, dialogs--CreateRoom, dialogs--InviteUsers, diff --git a/resources/styles/nheko.qss b/resources/styles/nheko.qss index ec3537b9..7644b8bb 100644 --- a/resources/styles/nheko.qss +++ b/resources/styles/nheko.qss @@ -104,6 +104,7 @@ Avatar { } dialogs--Logout, +dialogs--ReCaptcha, dialogs--LeaveRoom, dialogs--CreateRoom, dialogs--InviteUsers, diff --git a/src/LoginPage.cc b/src/LoginPage.cc index 9a048a75..b8207226 100644 --- a/src/LoginPage.cc +++ b/src/LoginPage.cc @@ -144,7 +144,7 @@ LoginPage::LoginPage(QSharedPointer client, QWidget *parent) connect(password_input_, SIGNAL(returnPressed()), login_button_, SLOT(click())); connect(serverInput_, SIGNAL(returnPressed()), login_button_, SLOT(click())); connect(client_.data(), SIGNAL(loginError(QString)), this, SLOT(loginError(QString))); - connect(client_.data(), SIGNAL(loginError(QString)), this, SIGNAL(errorOccured())); + connect(client_.data(), SIGNAL(loginError(QString)), this, SIGNAL(errorOccurred())); connect(matrixid_input_, SIGNAL(editingFinished()), this, SLOT(onMatrixIdEntered())); connect(client_.data(), SIGNAL(versionError(QString)), this, SLOT(versionError(QString))); connect(client_.data(), SIGNAL(versionSuccess()), this, SLOT(versionSuccess())); diff --git a/src/MainWindow.cc b/src/MainWindow.cc index 4bdd7819..5d5cb598 100644 --- a/src/MainWindow.cc +++ b/src/MainWindow.cc @@ -85,7 +85,12 @@ MainWindow::MainWindow(QWidget *parent) connect(login_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); connect(login_page_, &LoginPage::loggingIn, this, &MainWindow::showOverlayProgressBar); connect( - login_page_, &LoginPage::errorOccured, this, [this]() { removeOverlayProgressBar(); }); + register_page_, &RegisterPage::registering, this, &MainWindow::showOverlayProgressBar); + connect( + login_page_, &LoginPage::errorOccurred, this, [this]() { removeOverlayProgressBar(); }); + connect(register_page_, &RegisterPage::errorOccurred, this, [this]() { + removeOverlayProgressBar(); + }); connect(register_page_, SIGNAL(backButtonClicked()), this, SLOT(showWelcomePage())); connect(chat_page_, SIGNAL(close()), this, SLOT(showWelcomePage())); @@ -120,6 +125,11 @@ MainWindow::MainWindow(QWidget *parent) this, SLOT(showChatPage(QString, QString, QString))); + connect(client_.data(), + SIGNAL(registerSuccess(QString, QString, QString)), + this, + SLOT(showChatPage(QString, QString, QString))); + QShortcut *quitShortcut = new QShortcut(QKeySequence::Quit, this); connect(quitShortcut, &QShortcut::activated, this, QApplication::quit); diff --git a/src/MatrixClient.cc b/src/MatrixClient.cc index f4d2e66a..4a4fc67c 100644 --- a/src/MatrixClient.cc +++ b/src/MatrixClient.cc @@ -27,9 +27,10 @@ #include #include #include +#include +#include "Deserializable.h" #include "MatrixClient.h" -#include "Register.h" MatrixClient::MatrixClient(QString server, QObject *parent) : QNetworkAccessManager(parent) @@ -193,50 +194,66 @@ MatrixClient::logout() noexcept } void -MatrixClient::registerUser(const QString &user, const QString &pass, const QString &server) noexcept +MatrixClient::registerUser(const QString &user, + const QString &pass, + const QString &server, + const QString &session) noexcept { setServer(server); - QUrlQuery query; - query.addQueryItem("kind", "user"); - QUrl endpoint(server_); endpoint.setPath(clientApiUrl_ + "/register"); - endpoint.setQuery(query); QNetworkRequest request(QString(endpoint.toEncoded())); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); - RegisterRequest body(user, pass); - auto reply = post(request, body.serialize()); + QJsonObject body{{"username", user}, {"password", pass}}; - connect(reply, &QNetworkReply::finished, this, [this, reply]() { + // We trying to register using the response from the recaptcha. + if (!session.isEmpty()) + body = QJsonObject{ + {"username", user}, + {"password", pass}, + {"auth", QJsonObject{{"type", "m.login.recaptcha"}, {"session", session}}}}; + + auto reply = post(request, QJsonDocument(body).toJson(QJsonDocument::Compact)); + + connect(reply, &QNetworkReply::finished, this, [this, reply, user, pass, server]() { reply->deleteLater(); int status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); auto data = reply->readAll(); - auto json = QJsonDocument::fromJson(data); - if (status == 0 || status >= 400) { - if (json.isObject() && json.object().contains("error")) - emit registerError(json.object().value("error").toString()); - else - emit registerError(reply->errorString()); + // Try to parse a regular register response. + try { + mtx::responses::Register res = nlohmann::json::parse(data); + emit registerSuccess(QString::fromStdString(res.user_id.toString()), + QString::fromStdString(res.user_id.hostname()), + QString::fromStdString(res.access_token)); + } catch (const std::exception &e) { + qWarning() << "Register" << e.what(); + } + // Check if the server requires a registration flow. + try { + mtx::responses::RegistrationFlows res = nlohmann::json::parse(data); + emit registrationFlow( + user, pass, server, QString::fromStdString(res.session)); return; + } catch (const std::exception &) { } - RegisterResponse response; - - try { - response.deserialize(json); - emit registerSuccess(response.getUserId(), - response.getHomeServer(), - response.getAccessToken()); - } catch (DeserializationException &e) { - qWarning() << "Register" << e.what(); - emit registerError("Received malformed response."); + // We encountered an unknown error. + if (status == 0 || status >= 400) { + try { + mtx::errors::Error res = nlohmann::json::parse(data); + emit registerError(QString::fromStdString(res.error)); + return; + } catch (const std::exception &) { + } + + emit registerError(reply->errorString()); } }); } diff --git a/src/Register.cc b/src/Register.cc deleted file mode 100644 index d63f9229..00000000 --- a/src/Register.cc +++ /dev/null @@ -1,53 +0,0 @@ -/* - * nheko Copyright (C) 2017 Konstantinos Sideris - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include "Register.h" - -RegisterRequest::RegisterRequest(const QString &username, const QString &password) - : user_(username) - , password_(password) -{} - -QByteArray -RegisterRequest::serialize() noexcept -{ - QJsonObject body{{"username", user_}, {"password", password_}}; - - return QJsonDocument(body).toJson(QJsonDocument::Compact); -} - -void -RegisterResponse::deserialize(const QJsonDocument &data) -{ - if (!data.isObject()) - throw DeserializationException("Response is not a JSON object"); - - QJsonObject object = data.object(); - - if (!object.contains("access_token")) - throw DeserializationException("Missing access_token param"); - - if (!object.contains("home_server")) - throw DeserializationException("Missing home_server param"); - - if (!object.contains("user_id")) - throw DeserializationException("Missing user_id param"); - - access_token_ = object.value("access_token").toString(); - home_server_ = object.value("home_server").toString(); - user_id_ = object.value("user_id").toString(); -} diff --git a/src/RegisterPage.cc b/src/RegisterPage.cc index 044d2fcf..c0fa11a6 100644 --- a/src/RegisterPage.cc +++ b/src/RegisterPage.cc @@ -16,14 +16,18 @@ */ #include +#include #include "Config.h" #include "FlatButton.h" +#include "MainWindow.h" #include "MatrixClient.h" #include "RaisedButton.h" #include "RegisterPage.h" #include "TextField.h" +#include "dialogs/ReCaptcha.hpp" + RegisterPage::RegisterPage(QSharedPointer client, QWidget *parent) : QWidget(parent) , client_(client) @@ -126,6 +130,30 @@ RegisterPage::RegisterPage(QSharedPointer client, QWidget *parent) SIGNAL(registerError(const QString &)), this, SLOT(registerError(const QString &))); + connect(client_.data(), + &MatrixClient::registrationFlow, + this, + [this](const QString &user, + const QString &pass, + const QString &server, + const QString &session) { + emit errorOccurred(); + + if (!captchaDialog_) { + captchaDialog_ = + std::make_shared(server, session, this); + connect(captchaDialog_.get(), + &dialogs::ReCaptcha::closing, + this, + [this, user, pass, server, session]() { + captchaDialog_->close(); + emit registering(); + client_->registerUser(user, pass, server, session); + }); + } + + QTimer::singleShot(1000, this, [this]() { captchaDialog_->show(); }); + }); setLayout(top_layout_); } @@ -139,6 +167,7 @@ RegisterPage::onBackButtonClicked() void RegisterPage::registerError(const QString &msg) { + emit errorOccurred(); error_label_->setText(msg); } @@ -161,6 +190,7 @@ RegisterPage::onRegisterButtonClicked() QString server = server_input_->text(); client_->registerUser(username, password, server); + emit registering(); } } diff --git a/src/dialogs/ReCaptcha.cpp b/src/dialogs/ReCaptcha.cpp new file mode 100644 index 00000000..eb69a6a2 --- /dev/null +++ b/src/dialogs/ReCaptcha.cpp @@ -0,0 +1,75 @@ +#include +#include +#include +#include +#include + +#include "Config.h" +#include "FlatButton.h" +#include "RaisedButton.h" +#include "Theme.h" + +#include "dialogs/ReCaptcha.hpp" + +using namespace dialogs; + +ReCaptcha::ReCaptcha(const QString &server, const QString &session, QWidget *parent) + : QWidget(parent) +{ + setAutoFillBackground(true); + setWindowFlags(Qt::Tool | Qt::WindowStaysOnTopHint); + setWindowModality(Qt::WindowModal); + + auto layout = new QVBoxLayout(this); + layout->setSpacing(30); + layout->setMargin(20); + + auto buttonLayout = new QHBoxLayout(); + buttonLayout->setSpacing(8); + buttonLayout->setMargin(0); + + openCaptchaBtn_ = new FlatButton("OPEN reCAPTCHA", this); + openCaptchaBtn_->setFontSize(conf::btn::fontSize); + + confirmBtn_ = new RaisedButton(tr("CONFIRM"), this); + confirmBtn_->setFontSize(conf::btn::fontSize); + + cancelBtn_ = new RaisedButton(tr("CANCEL"), this); + cancelBtn_->setFontSize(conf::btn::fontSize); + + buttonLayout->addStretch(1); + buttonLayout->addWidget(openCaptchaBtn_); + buttonLayout->addWidget(confirmBtn_); + buttonLayout->addWidget(cancelBtn_); + + QFont font; + font.setPixelSize(conf::headerFontSize); + + auto label = new QLabel(tr("Solve the reCAPTCHA and press the confirm button"), this); + label->setFont(font); + + layout->addWidget(label); + layout->addLayout(buttonLayout); + + connect(openCaptchaBtn_, &QPushButton::clicked, [server, session, this]() { + const auto url = + QString( + "https://%1/_matrix/client/r0/auth/m.login.recaptcha/fallback/web?session=%2") + .arg(server) + .arg(session); + + QDesktopServices::openUrl(url); + }); + + connect(confirmBtn_, &QPushButton::clicked, this, &dialogs::ReCaptcha::closing); + connect(cancelBtn_, &QPushButton::clicked, this, &dialogs::ReCaptcha::close); +} + +void +ReCaptcha::paintEvent(QPaintEvent *) +{ + QStyleOption opt; + opt.init(this); + QPainter p(this); + style()->drawPrimitive(QStyle::PE_Widget, &opt, &p, this); +}