File indexing completed on 2024-04-28 04:59:48

0001 // SPDX-FileCopyrightText: 2023 Tobias Fella <tobias.fella@kde.org>
0002 // SPDX-License-Identifier: GPL-2.0-or-later
0003 
0004 #include "registration.h"
0005 
0006 #include <QTcpServer>
0007 #include <QTcpSocket>
0008 #include <QThread>
0009 
0010 #include <Quotient/csapi/registration.h>
0011 #include <Quotient/qt_connection_util.h>
0012 #include <Quotient/settings.h>
0013 
0014 #include "controller.h"
0015 #include "login.h"
0016 
0017 #include <KLocalizedString>
0018 
0019 using namespace Quotient;
0020 
0021 Registration::Registration()
0022 {
0023     auto server = new QTcpServer(this);
0024     server->listen(QHostAddress("127.0.0.1"_ls), 20847);
0025     connect(server, &QTcpServer::newConnection, this, [this, server]() {
0026         auto conn = server->nextPendingConnection();
0027         connect(conn, &QIODevice::readyRead, this, [this, conn]() {
0028             auto code =
0029                 "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
0030                     .arg(m_recaptchaSiteKey);
0031             conn->write(code.toLatin1().data(), code.length());
0032             conn->close();
0033         });
0034     });
0035 
0036     connect(this, &Registration::homeserverChanged, this, &Registration::testHomeserver);
0037     connect(this, &Registration::usernameChanged, this, &Registration::testUsername);
0038 }
0039 
0040 void Registration::setRecaptchaResponse(const QString &recaptchaResponse)
0041 {
0042     m_recaptchaResponse = recaptchaResponse;
0043     Q_EMIT recaptchaResponseChanged();
0044     registerAccount();
0045 }
0046 
0047 QString Registration::recaptchaResponse() const
0048 {
0049     return m_recaptchaResponse;
0050 }
0051 
0052 void Registration::setRecaptchaSiteKey(const QString &recaptchaSiteKey)
0053 {
0054     m_recaptchaSiteKey = recaptchaSiteKey;
0055     Q_EMIT recaptchaSiteKeyChanged();
0056 }
0057 
0058 QString Registration::recaptchaSiteKey() const
0059 {
0060     return m_recaptchaSiteKey;
0061 }
0062 
0063 void Registration::registerAccount()
0064 {
0065     setStatus(Working);
0066     Omittable<QJsonObject> authData = none;
0067     if (nextStep() == "m.login.recaptcha"_ls) {
0068         authData = QJsonObject{
0069             {"type"_ls, "m.login.recaptcha"_ls},
0070             {"response"_ls, m_recaptchaResponse},
0071             {"session"_ls, m_session},
0072         };
0073     } else if (nextStep() == "m.login.terms"_ls) {
0074         authData = QJsonObject{
0075             {"type"_ls, "m.login.terms"_ls},
0076             {"session"_ls, m_session},
0077         };
0078     } else if (nextStep() == "m.login.email.identity"_ls) {
0079         authData = QJsonObject{
0080             {"type"_ls, "m.login.email.identity"_ls},
0081             {"threepid_creds"_ls,
0082              QJsonObject{
0083                  {"sid"_ls, m_sid},
0084                  {"client_secret"_ls, m_emailSecret},
0085              }},
0086             {"session"_ls, m_session},
0087         };
0088     }
0089     auto job = m_connection->callApi<NeoChatRegisterJob>("user"_ls, authData, m_username, m_password, QString(), QString(), true);
0090     connect(job, &BaseJob::result, this, [this, job]() {
0091         if (job->status() == BaseJob::Success) {
0092             setNextStep("loading"_ls);
0093             auto connection = new NeoChatConnection(this);
0094             auto matrixId = "@%1:%2"_ls.arg(m_username, m_homeserver);
0095             connection->resolveServer(matrixId);
0096 
0097             auto displayName = "NeoChat %1 %2 %3 %4"_ls.arg(QSysInfo::machineHostName(),
0098                                                             QSysInfo::productType(),
0099                                                             QSysInfo::productVersion(),
0100                                                             QSysInfo::currentCpuArchitecture());
0101             connection->loginWithPassword(matrixId, m_password, displayName);
0102 
0103             connect(connection, &Connection::connected, this, [this, displayName, connection] {
0104                 AccountSettings account(connection->userId());
0105                 account.setKeepLoggedIn(true);
0106                 account.setHomeserver(connection->homeserver());
0107                 account.setDeviceId(connection->deviceId());
0108                 account.setDeviceName(displayName);
0109                 if (!Controller::instance().saveAccessTokenToKeyChain(account, connection->accessToken())) {
0110                     qWarning() << "Couldn't save access token";
0111                 }
0112                 account.sync();
0113                 Controller::instance().addConnection(connection);
0114                 Controller::instance().setActiveConnection(connection);
0115                 connectSingleShot(connection, &Connection::syncDone, this, []() {
0116                     Q_EMIT LoginHelper::instance().loaded();
0117                 });
0118                 m_connection = nullptr;
0119             });
0120 
0121             return;
0122         }
0123         const auto &data = job->jsonData();
0124         m_session = data["session"_ls].toString();
0125         const auto &params = data["params"_ls].toObject();
0126 
0127         // I'm not motivated enough to figure out how we should handle the flow stuff, so:
0128         // 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.
0129         // Otherwise, we're using the first flow.
0130         auto selectedFlow = data["flows"_ls].toArray()[0].toObject()["stages"_ls].toArray();
0131         for (const auto &flow : data["flows"_ls].toArray()) {
0132             if (flow.toObject()["stages"_ls].toArray().contains("m.login.email.identity"_ls)) {
0133                 selectedFlow = flow.toObject()["stages"_ls].toArray();
0134             }
0135         }
0136 
0137         setNextStep(selectedFlow[data["completed"_ls].toArray().size()].toString());
0138         m_recaptchaSiteKey = params["m.login.recaptcha"_ls]["public_key"_ls].toString();
0139         Q_EMIT recaptchaSiteKeyChanged();
0140         m_terms.clear();
0141         for (const auto &term : params["m.login.terms"_ls]["policies"_ls].toObject().keys()) {
0142             QVariantMap termData;
0143             termData["title"_ls] = params["m.login.terms"_ls]["policies"_ls][term]["en"_ls]["name"_ls].toString();
0144             termData["url"_ls] = params["m.login.terms"_ls]["policies"_ls][term]["en"_ls]["url"_ls].toString();
0145             m_terms += termData;
0146             Q_EMIT termsChanged();
0147         }
0148     });
0149 }
0150 
0151 QString Registration::homeserver() const
0152 {
0153     return m_homeserver;
0154 }
0155 
0156 void Registration::setHomeserver(const QString &url)
0157 {
0158     m_homeserver = url;
0159     Q_EMIT homeserverChanged();
0160 }
0161 
0162 void Registration::testHomeserver()
0163 {
0164     if (m_homeserver.isEmpty()) {
0165         setStatus(NoServer);
0166         return;
0167     }
0168     setStatus(TestingHomeserver);
0169     if (m_connection) {
0170         delete m_connection;
0171     }
0172 
0173     m_connection = new Connection(this);
0174     m_connection->resolveServer("@user:%1"_ls.arg(m_homeserver));
0175     connectSingleShot(m_connection.data(), &Connection::loginFlowsChanged, this, [this]() {
0176         if (m_testServerJob) {
0177             delete m_testServerJob;
0178         }
0179         m_testServerJob = m_connection->callApi<NeoChatRegisterJob>("user"_ls, none, "user"_ls, QString(), QString(), QString(), false);
0180         connect(m_testServerJob.data(), &BaseJob::finished, this, [this]() {
0181             if (m_testServerJob->error() == BaseJob::StatusCode::ContentAccessError) {
0182                 setStatus(ServerNoRegistration);
0183                 return;
0184             }
0185             if (m_testServerJob->status().code != 106) {
0186                 setStatus(InvalidServer);
0187                 return;
0188             }
0189             if (!m_username.isEmpty()) {
0190                 setStatus(TestingUsername);
0191                 testUsername();
0192             } else {
0193                 setStatus(NoUsername);
0194             }
0195         });
0196     });
0197 }
0198 
0199 void Registration::setUsername(const QString &username)
0200 {
0201     m_username = username;
0202     Q_EMIT usernameChanged();
0203 }
0204 
0205 QString Registration::username() const
0206 {
0207     return m_username;
0208 }
0209 
0210 void Registration::testUsername()
0211 {
0212     if (status() <= ServerNoRegistration) {
0213         return;
0214     }
0215     setStatus(TestingUsername);
0216     if (m_usernameJob) {
0217         m_usernameJob->abandon();
0218     }
0219     if (m_username.isEmpty()) {
0220         setStatus(NoUsername);
0221         return;
0222     }
0223 
0224     m_usernameJob = m_connection->callApi<CheckUsernameAvailabilityJob>(m_username);
0225     connect(m_usernameJob, &BaseJob::result, this, [this]() {
0226         setStatus(m_usernameJob->error() == BaseJob::StatusCode::Success && *m_usernameJob->available() ? Ready : UsernameTaken);
0227     });
0228 }
0229 
0230 QList<QVariantMap> Registration::terms() const
0231 {
0232     return m_terms;
0233 }
0234 
0235 QString Registration::password() const
0236 {
0237     return m_password;
0238 }
0239 
0240 void Registration::setPassword(const QString &password)
0241 {
0242     m_password = password;
0243     Q_EMIT passwordChanged();
0244 }
0245 
0246 NeoChatRegisterJob::NeoChatRegisterJob(const QString &kind,
0247                                        const Omittable<QJsonObject> &auth,
0248                                        const QString &username,
0249                                        const QString &password,
0250                                        const QString &deviceId,
0251                                        const QString &initialDeviceDisplayName,
0252                                        Omittable<bool> inhibitLogin)
0253     : BaseJob(HttpVerb::Post, "RegisterJob"_ls, QByteArrayLiteral("/_matrix/client/r0/register"), false)
0254 {
0255     QJsonObject _data;
0256     if (auth) {
0257         addParam<>(_data, "auth"_ls, auth);
0258     }
0259     addParam<>(_data, "username"_ls, username);
0260     addParam<IfNotEmpty>(_data, "password"_ls, password);
0261     addParam<IfNotEmpty>(_data, "device_id"_ls, deviceId);
0262     addParam<IfNotEmpty>(_data, "initial_device_display_name"_ls, initialDeviceDisplayName);
0263     addParam<IfNotEmpty>(_data, "inhibit_login"_ls, inhibitLogin);
0264     addParam<IfNotEmpty>(_data, "kind"_ls, kind);
0265     addParam<IfNotEmpty>(_data, "refresh_token"_ls, false);
0266     setRequestData(_data);
0267 }
0268 
0269 QString Registration::email() const
0270 {
0271     return m_email;
0272 }
0273 
0274 void Registration::setEmail(const QString &email)
0275 {
0276     m_email = email;
0277     Q_EMIT emailChanged();
0278 }
0279 
0280 QString Registration::nextStep() const
0281 {
0282     return m_nextStep;
0283 }
0284 
0285 void Registration::setNextStep(const QString &nextStep)
0286 {
0287     m_nextStep = nextStep;
0288     Q_EMIT nextStepChanged();
0289 }
0290 
0291 Registration::Status Registration::status() const
0292 {
0293     return m_status;
0294 }
0295 
0296 QString Registration::statusString() const
0297 {
0298     switch (m_status) {
0299     case NoServer:
0300         return i18n("No server.");
0301     case TestingHomeserver:
0302         return i18n("Checking Server availability.");
0303     case InvalidServer:
0304         return i18n("This is not a valid server.");
0305     case ServerNoRegistration:
0306         return i18n("Registration for this server is disabled.");
0307     case NoUsername:
0308         return i18n("No username.");
0309     case TestingUsername:
0310         return i18n("Checking username availability.");
0311     case UsernameTaken:
0312         return i18n("This username is not available.");
0313     case Ready:
0314         return i18n("Continue");
0315     case Working:
0316         return i18n("Working");
0317     }
0318     return {};
0319 }
0320 
0321 void Registration::setStatus(Status status)
0322 {
0323     m_status = status;
0324     Q_EMIT statusChanged();
0325 }
0326 
0327 void Registration::registerEmail()
0328 {
0329     m_emailSecret = QString::fromLatin1(QUuid::createUuid().toString().toLatin1().toBase64());
0330     EmailValidationData data;
0331     data.email = m_email;
0332     data.clientSecret = m_emailSecret;
0333     data.sendAttempt = 0;
0334 
0335     auto job = m_connection->callApi<RequestTokenToRegisterEmailJob>(data);
0336     connect(job, &BaseJob::finished, this, [this, job]() {
0337         m_sid = job->jsonData()["sid"_ls].toString();
0338     });
0339 }
0340 
0341 #include "moc_registration.cpp"