Implement user registration with reCAPTCHA

fixes #264
This commit is contained in:
Konstantinos Sideris 2018-03-12 22:23:26 +02:00
parent 39a8150fae
commit 4659d0efc2
17 changed files with 212 additions and 137 deletions

View File

@ -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

View File

@ -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).

View File

@ -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}

View File

@ -41,7 +41,7 @@ public:
signals:
void backButtonClicked();
void loggingIn();
void errorOccured();
void errorOccurred();
protected:
void paintEvent(QPaintEvent *event) override;

View File

@ -49,6 +49,7 @@ class InviteUsers;
class JoinRoom;
class LeaveRoom;
class Logout;
class ReCaptcha;
}
class MainWindow : public QMainWindow

View File

@ -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();

View File

@ -1,52 +0,0 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "Deserializable.h"
#include <QJsonDocument>
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_;
};

View File

@ -20,12 +20,17 @@
#include <QLabel>
#include <QLayout>
#include <QSharedPointer>
#include <memory>
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<MatrixClient> client_;
//! ReCaptcha dialog.
std::shared_ptr<dialogs::ReCaptcha> captchaDialog_;
};

View File

@ -0,0 +1,28 @@
#pragma once
#include <QWidget>
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

View File

@ -101,6 +101,7 @@ Avatar {
}
dialogs--Logout,
dialogs--ReCaptcha,
dialogs--LeaveRoom,
dialogs--CreateRoom,
dialogs--InviteUsers,

View File

@ -104,6 +104,7 @@ Avatar {
}
dialogs--Logout,
dialogs--ReCaptcha,
dialogs--LeaveRoom,
dialogs--CreateRoom,
dialogs--InviteUsers,

View File

@ -144,7 +144,7 @@ LoginPage::LoginPage(QSharedPointer<MatrixClient> 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()));

View File

@ -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);

View File

@ -27,9 +27,10 @@
#include <QPixmap>
#include <QSettings>
#include <QUrlQuery>
#include <mtx/errors.hpp>
#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());
return;
// 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();
}
RegisterResponse response;
// Check if the server requires a registration flow.
try {
response.deserialize(json);
emit registerSuccess(response.getUserId(),
response.getHomeServer(),
response.getAccessToken());
} catch (DeserializationException &e) {
qWarning() << "Register" << e.what();
emit registerError("Received malformed response.");
mtx::responses::RegistrationFlows res = nlohmann::json::parse(data);
emit registrationFlow(
user, pass, server, QString::fromStdString(res.session));
return;
} catch (const std::exception &) {
}
// 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());
}
});
}

View File

@ -1,53 +0,0 @@
/*
* nheko Copyright (C) 2017 Konstantinos Sideris <siderisk@auth.gr>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#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();
}

View File

@ -16,14 +16,18 @@
*/
#include <QStyleOption>
#include <QTimer>
#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<MatrixClient> client, QWidget *parent)
: QWidget(parent)
, client_(client)
@ -126,6 +130,30 @@ RegisterPage::RegisterPage(QSharedPointer<MatrixClient> 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<dialogs::ReCaptcha>(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();
}
}

75
src/dialogs/ReCaptcha.cpp Normal file
View File

@ -0,0 +1,75 @@
#include <QDesktopServices>
#include <QLabel>
#include <QPaintEvent>
#include <QStyleOption>
#include <QVBoxLayout>
#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);
}