From 94f3d61302f280678d465ef33e6faec1498ed579 Mon Sep 17 00:00:00 2001 From: 0xf8 <0xf8.dev@proton.me> Date: Tue, 20 Jun 2023 14:35:02 -0400 Subject: [PATCH] Add custom login support --- PollyMC.iml | 12 ++ launcher/CMakeLists.txt | 6 + launcher/minecraft/MinecraftInstance.cpp | 6 +- launcher/minecraft/auth/AccountData.cpp | 16 ++- launcher/minecraft/auth/AccountData.h | 5 +- launcher/minecraft/auth/AuthSession.h | 2 + launcher/minecraft/auth/MinecraftAccount.cpp | 28 ++++ launcher/minecraft/auth/MinecraftAccount.h | 17 +++ launcher/minecraft/auth/Yggdrasil.cpp | 2 + launcher/minecraft/auth/Yggdrasil.h | 2 + launcher/minecraft/auth/flows/Custom.cpp | 25 ++++ launcher/minecraft/auth/flows/Custom.h | 27 ++++ .../auth/steps/CustomProfileStep.cpp | 94 +++++++++++++ .../minecraft/auth/steps/CustomProfileStep.h | 22 +++ launcher/minecraft/auth/steps/CustomStep.cpp | 52 +++++++ launcher/minecraft/auth/steps/CustomStep.h | 28 ++++ launcher/minecraft/launch/InjectAuthlib.cpp | 5 +- launcher/minecraft/launch/InjectAuthlib.h | 1 + launcher/ui/dialogs/CustomLoginDialog.cpp | 131 ++++++++++++++++++ launcher/ui/dialogs/CustomLoginDialog.h | 60 ++++++++ launcher/ui/dialogs/CustomLoginDialog.ui | 94 +++++++++++++ launcher/ui/pages/global/AccountListPage.cpp | 23 ++- launcher/ui/pages/global/AccountListPage.h | 1 + launcher/ui/pages/global/AccountListPage.ui | 6 + 24 files changed, 653 insertions(+), 12 deletions(-) create mode 100644 PollyMC.iml create mode 100644 launcher/minecraft/auth/flows/Custom.cpp create mode 100644 launcher/minecraft/auth/flows/Custom.h create mode 100644 launcher/minecraft/auth/steps/CustomProfileStep.cpp create mode 100644 launcher/minecraft/auth/steps/CustomProfileStep.h create mode 100644 launcher/minecraft/auth/steps/CustomStep.cpp create mode 100644 launcher/minecraft/auth/steps/CustomStep.h create mode 100644 launcher/ui/dialogs/CustomLoginDialog.cpp create mode 100644 launcher/ui/dialogs/CustomLoginDialog.h create mode 100644 launcher/ui/dialogs/CustomLoginDialog.ui diff --git a/PollyMC.iml b/PollyMC.iml new file mode 100644 index 00000000..03682804 --- /dev/null +++ b/PollyMC.iml @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 1a2383e9..eea16924 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -222,6 +222,8 @@ set(MINECRAFT_SOURCES minecraft/auth/flows/Offline.h minecraft/auth/flows/Elyby.cpp minecraft/auth/flows/Elyby.h + minecraft/auth/flows/Custom.cpp + minecraft/auth/flows/Custom.h minecraft/auth/steps/EntitlementsStep.cpp minecraft/auth/steps/EntitlementsStep.h @@ -229,6 +231,10 @@ set(MINECRAFT_SOURCES minecraft/auth/steps/ElybyProfileStep.h minecraft/auth/steps/ElybyStep.cpp minecraft/auth/steps/ElybyStep.h + minecraft/auth/steps/CustomProfileStep.cpp + minecraft/auth/steps/CustomProfileStep.h + minecraft/auth/steps/CustomStep.cpp + minecraft/auth/steps/CustomStep.h minecraft/auth/steps/GetSkinStep.cpp minecraft/auth/steps/GetSkinStep.h minecraft/auth/steps/LauncherLoginStep.cpp diff --git a/launcher/minecraft/MinecraftInstance.cpp b/launcher/minecraft/MinecraftInstance.cpp index 8f60510b..a42189c9 100644 --- a/launcher/minecraft/MinecraftInstance.cpp +++ b/launcher/minecraft/MinecraftInstance.cpp @@ -1031,7 +1031,11 @@ shared_qobject_ptr MinecraftInstance::createLaunchTask(AuthSessionPt // authlib patch if (session->user_type == "elyby") { - process->appendStep(makeShared(pptr, &m_injector)); + process->appendStep(makeShared(pptr, &m_injector, "ely.by")); + } + else if (session->user_type == "custom") + { + process->appendStep(makeShared(pptr, &m_injector, session.url)); } process->appendStep(makeShared(pptr, Net::Mode::Online)); diff --git a/launcher/minecraft/auth/AccountData.cpp b/launcher/minecraft/auth/AccountData.cpp index 50ca155f..c5aeee63 100644 --- a/launcher/minecraft/auth/AccountData.cpp +++ b/launcher/minecraft/auth/AccountData.cpp @@ -354,6 +354,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) { type = AccountType::Offline; } else if (typeS == "Elyby") { type = AccountType::Elyby; + } else if (typeS == "Custom") { + type = AccountType::Custom } else { qWarning() << "Failed to parse account data: type is not recognized."; return false; @@ -414,6 +416,9 @@ QJsonObject AccountData::saveState() const { else if (type == AccountType::Elyby) { output["type"] = "Elyby"; } + else if (type == AccountType::Custom) { + output["type"] = "Custom"; + } tokenToJSONV3(output, yggdrasilToken, "ygg"); profileToJSONV3(output, minecraftProfile, "profile"); @@ -433,14 +438,14 @@ QString AccountData::accessToken() const { } QString AccountData::clientToken() const { - if(type != AccountType::Mojang && type != AccountType::Elyby) { + if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) { return QString(); } return yggdrasilToken.extra["clientToken"].toString(); } void AccountData::setClientToken(QString clientToken) { - if(type != AccountType::Mojang && type != AccountType::Elyby) { + if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) { return; } yggdrasilToken.extra["clientToken"] = clientToken; @@ -454,7 +459,7 @@ void AccountData::generateClientTokenIfMissing() { } void AccountData::invalidateClientToken() { - if(type != AccountType::Mojang && type != AccountType::Elyby) { + if(type != AccountType::Mojang && type != AccountType::Elyby && type != AccountType::Custom) { return; } yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]")); @@ -475,12 +480,11 @@ QString AccountData::profileName() const { QString AccountData::accountDisplayString() const { switch(type) { + case AccountType::Custom: + case AccountType::Elyby: case AccountType::Mojang: { return userName(); } - case AccountType::Elyby: { - return userName(); - } case AccountType::Offline: { return QObject::tr(""); } diff --git a/launcher/minecraft/auth/AccountData.h b/launcher/minecraft/auth/AccountData.h index c11aa146..895899a0 100644 --- a/launcher/minecraft/auth/AccountData.h +++ b/launcher/minecraft/auth/AccountData.h @@ -75,7 +75,8 @@ enum class AccountType { MSA, Mojang, Offline, - Elyby + Elyby, + Custom }; enum class AccountState { @@ -114,6 +115,8 @@ struct AccountData { QString lastError() const; + QString customUrl const; + AccountType type = AccountType::MSA; bool legacy = false; bool canMigrateToMSA = false; diff --git a/launcher/minecraft/auth/AuthSession.h b/launcher/minecraft/auth/AuthSession.h index a75df506..35107c4b 100644 --- a/launcher/minecraft/auth/AuthSession.h +++ b/launcher/minecraft/auth/AuthSession.h @@ -40,6 +40,8 @@ struct AuthSession QString uuid; // 'legacy' or 'mojang', depending on account type QString user_type; + // Yggdrasil server url + QString url; // Did the auth server reply? bool auth_server_online = false; // Did the user request online mode? diff --git a/launcher/minecraft/auth/MinecraftAccount.cpp b/launcher/minecraft/auth/MinecraftAccount.cpp index c8591fc3..85a387e4 100644 --- a/launcher/minecraft/auth/MinecraftAccount.cpp +++ b/launcher/minecraft/auth/MinecraftAccount.cpp @@ -52,6 +52,7 @@ #include "flows/Mojang.h" #include "flows/Offline.h" #include "flows/Elyby.h" +#include "flows/Custom.h" MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) { data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); @@ -118,6 +119,17 @@ MinecraftAccountPtr MinecraftAccount::createElyby(const QString &username) return account; } +MinecraftAccountPtr MinecraftAccount::createCustom(const QString &username, const QString &url) +{ + MinecraftAccountPtr account = makeShared(); + + account->data.type = AccountType::Custom; + account->data.yggdrasilToken.extra["userName"] = username; + account->data.yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]")); + account->data.minecraftEntitlement.ownsMinecraft = true; + account->data.minecraftEntitlement.canPlayMinecraft = true; + return account; +} QJsonObject MinecraftAccount::saveToJson() const { @@ -185,6 +197,17 @@ shared_qobject_ptr MinecraftAccount::loginElyby(QString password) { return m_currentTask; } +shared_qobject_ptr MinecraftAccount::loginCustom(QString password, QString url) { + Q_ASSERT(m_currentTask.get() == nullptr); + + m_currentTask.reset(new customLogin(&data, password, url)); + connect(m_currentTask.get(), SIGNAL(succeeded()), SLOT(authSucceeded())); + connect(m_currentTask.get(), SIGNAL(failed(QString)), SLOT(authFailed(QString))); + connect(m_currentTask.get(), &Task::aborted, this, [this]{ authFailed(tr("Aborted")); }); + emit activityChanged(true); + return m_currentTask; +} + shared_qobject_ptr MinecraftAccount::refresh() { if(m_currentTask) { return m_currentTask; @@ -199,6 +222,9 @@ shared_qobject_ptr MinecraftAccount::refresh() { else if(data.type == AccountType::Elyby) { m_currentTask.reset(new ElybyRefresh(&data)); } + else if (data.type == AccountType::Custom) { + m_currentTask.reset(new CustomRefresh(&data)); + } else { m_currentTask.reset(new MojangRefresh(&data)); } @@ -328,6 +354,8 @@ void MinecraftAccount::fillSession(AuthSessionPtr session) session->uuid = data.profileId(); // 'legacy' or 'mojang', depending on account type session->user_type = typeString(); + session->url = data.customUrl; + if (!session->access_token.isEmpty()) { session->session = "token:" + data.accessToken() + ":" + data.profileId(); diff --git a/launcher/minecraft/auth/MinecraftAccount.h b/launcher/minecraft/auth/MinecraftAccount.h index 4985ab97..3bb50c5e 100644 --- a/launcher/minecraft/auth/MinecraftAccount.h +++ b/launcher/minecraft/auth/MinecraftAccount.h @@ -97,6 +97,8 @@ public: /* construction */ static MinecraftAccountPtr createElyby(const QString &username); + static MinecraftAccountPtr createCustom(const QString &username, const QString &url); + static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json); static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json); @@ -117,6 +119,8 @@ public: /* manipulation */ shared_qobject_ptr loginElyby(QString password); + shared_qobject_ptr loginCustom(QString password, QString url); + shared_qobject_ptr refresh(); shared_qobject_ptr currentTask(); @@ -146,6 +150,10 @@ public: /* queries */ return data.profileName(); } + qString customUrl() const { + return data.customUrl(); + } + bool isActive() const; bool canMigrate() const { @@ -168,6 +176,10 @@ public: /* queries */ return data.type == AccountType::Elyby; } + bool isCustom() const { + return data.type == AccountType::Custom; + } + bool ownsMinecraft() const { return data.minecraftEntitlement.ownsMinecraft; } @@ -192,10 +204,15 @@ public: /* queries */ case AccountType::Offline: { return "offline"; } + break; case AccountType::Elyby: { return "elyby"; } break; + case AccountType::Custom { + return "custom"; + } + break; default: { return "unknown"; } diff --git a/launcher/minecraft/auth/Yggdrasil.cpp b/launcher/minecraft/auth/Yggdrasil.cpp index acc026be..28e50145 100644 --- a/launcher/minecraft/auth/Yggdrasil.cpp +++ b/launcher/minecraft/auth/Yggdrasil.cpp @@ -129,6 +129,8 @@ void Yggdrasil::login(QString password, QString baseUrl) { QJsonDocument doc(req); + this->customUrl = baseUrl; + QUrl reqUrl(baseUrl + "authenticate"); QNetworkRequest netRequest(reqUrl); QByteArray requestData = doc.toJson(); diff --git a/launcher/minecraft/auth/Yggdrasil.h b/launcher/minecraft/auth/Yggdrasil.h index 34eb18b2..505917a6 100644 --- a/launcher/minecraft/auth/Yggdrasil.h +++ b/launcher/minecraft/auth/Yggdrasil.h @@ -97,6 +97,8 @@ protected: QTimer counter; int count = 0; // num msec since time reset + QString customUrl; + const int timeout_max = 30000; const int time_step = 50; }; diff --git a/launcher/minecraft/auth/flows/Custom.cpp b/launcher/minecraft/auth/flows/Custom.cpp new file mode 100644 index 00000000..2b9b7654 --- /dev/null +++ b/launcher/minecraft/auth/flows/Custom.cpp @@ -0,0 +1,25 @@ +#include "Custom.h" + +#include "minecraft/auth/steps/CustomStep.h" +#include "minecraft/auth/steps/CustomProfileStep.h" +#include "minecraft/auth/steps/GetSkinStep.h" + +customRefresh::customRefresh( + AccountData *data, + QObject *parent +) : AuthFlow(data, parent) { + m_steps.append(makeShared(m_data, QString())); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); +} + +customLogin::customLogin( + AccountData *data, + QString password, + QString url, + QObject *parent +): AuthFlow(data, parent), m_password(password), m_url(url) { + m_steps.append(makeShared(m_data, m_password, m_url)); + m_steps.append(makeShared(m_data)); + m_steps.append(makeShared(m_data)); +} diff --git a/launcher/minecraft/auth/flows/Custom.h b/launcher/minecraft/auth/flows/Custom.h new file mode 100644 index 00000000..01c0f2f5 --- /dev/null +++ b/launcher/minecraft/auth/flows/Custom.h @@ -0,0 +1,27 @@ +#pragma once +#include "AuthFlow.h" + +class customRefresh : public AuthFlow +{ + Q_OBJECT +public: + explicit customRefresh( + AccountData *data, + QObject *parent = 0 + ); +}; + +class customLogin : public AuthFlow +{ + Q_OBJECT +public: + explicit customLogin( + AccountData *data, + QString password, + QString url, + QObject *parent = 0 + ); + +private: + QString m_password; +}; diff --git a/launcher/minecraft/auth/steps/CustomProfileStep.cpp b/launcher/minecraft/auth/steps/CustomProfileStep.cpp new file mode 100644 index 00000000..496a4a7a --- /dev/null +++ b/launcher/minecraft/auth/steps/CustomProfileStep.cpp @@ -0,0 +1,94 @@ +#include "CustomProfileStep.h" +#include + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "net/NetUtils.h" + +CustomProfileStep::CustomProfileStep(AccountData* data) : AuthStep(data) { + +} + +CustomProfileStep::~CustomProfileStep() noexcept = default; + +QString CustomProfileStep::describe() { + return tr("Fetching the Minecraft profile."); +} + + +void CustomProfileStep::perform() { + if (m_data->minecraftProfile.id.isEmpty()) { + emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile.")); + return; + } + +// m_data-> + + QUrl url = QUrl(m_data.customUrl + "/session/profile/" + m_data->minecraftProfile.id); + QNetworkRequest req = QNetworkRequest(url); + AuthRequest *request = new AuthRequest(this); + connect(request, &AuthRequest::finished, this, &CustomProfileStep::onRequestDone); + request->get(req); +} + +void CustomProfileStep::rehydrate() { + // NOOP, for now. We only save bools and there's nothing to check. +} + +void CustomProfileStep::onRequestDone( + QNetworkReply::NetworkError error, + QByteArray data, + QList headers +) { + auto requestor = qobject_cast(QObject::sender()); + requestor->deleteLater(); + +#ifndef NDEBUG + qDebug() << data; +#endif + if (error == QNetworkReply::ContentNotFoundError) { + // NOTE: Succeed even if we do not have a profile. This is a valid account state. + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_SUCCEEDED, + tr("Account has no Minecraft profile.") + ); + return; + } + if (error != QNetworkReply::NoError) { + qWarning() << "Error getting profile:"; + qWarning() << " HTTP Status: " << requestor->httpStatus_; + qWarning() << " Internal error no.: " << error; + qWarning() << " Error string: " << requestor->errorString_; + + qWarning() << " Response:"; + qWarning() << QString::fromUtf8(data); + + if (Net::isApplicationError(error)) { + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + else { + emit finished( + AccountTaskState::STATE_OFFLINE, + tr("Minecraft Java profile acquisition failed: %1").arg(requestor->errorString_) + ); + } + return; + } + if(!Parsers::parseMinecraftProfileMojang(data, m_data->minecraftProfile)) { + m_data->minecraftProfile = MinecraftProfile(); + emit finished( + AccountTaskState::STATE_FAILED_SOFT, + tr("Minecraft Java profile response could not be parsed") + ); + return; + } + + emit finished( + AccountTaskState::STATE_WORKING, + tr("Minecraft Java profile acquisition succeeded.") + ); +} diff --git a/launcher/minecraft/auth/steps/CustomProfileStep.h b/launcher/minecraft/auth/steps/CustomProfileStep.h new file mode 100644 index 00000000..725a829e --- /dev/null +++ b/launcher/minecraft/auth/steps/CustomProfileStep.h @@ -0,0 +1,22 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + + +class CustomProfileStep : public AuthStep { + Q_OBJECT + +public: + explicit CustomProfileStep(AccountData *data); + virtual ~CustomProfileStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList); +}; diff --git a/launcher/minecraft/auth/steps/CustomStep.cpp b/launcher/minecraft/auth/steps/CustomStep.cpp new file mode 100644 index 00000000..54298ed9 --- /dev/null +++ b/launcher/minecraft/auth/steps/CustomStep.cpp @@ -0,0 +1,52 @@ +#include "CustomStep.h" + +#include "minecraft/auth/AuthRequest.h" +#include "minecraft/auth/Parsers.h" +#include "minecraft/auth/Yggdrasil.h" + +CustomStep::CustomStep(AccountData* data, QString password, QString url) : AuthStep(data), m_password(password), m_url(url) { + m_yggdrasil = new Yggdrasil(m_data, this); + + connect(m_yggdrasil, &Task::failed, this, &CustomStep::onAuthFailed); + connect(m_yggdrasil, &Task::succeeded, this, &CustomStep::onAuthSucceeded); + connect(m_yggdrasil, &Task::aborted, this, &CustomStep::onAuthFailed); +} + +CustomStep::~CustomStep() noexcept = default; + +QString CustomStep::describe() { + return tr("Logging in with Custom account."); +} + +void CustomStep::rehydrate() { + // NOOP, for now. +} + +void CustomStep::perform() { + if(m_password.size()) { + m_yggdrasil->login(m_password, m_url + "/auth/"); + } + else { + m_yggdrasil->refresh(m_url + "/auth/"); + } +} + +void CustomStep::onAuthSucceeded() { + emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Custom")); +} + +void CustomStep::onAuthFailed() { + // TODO: hook these in again, expand to MSA + // m_error = m_yggdrasil->m_error; + // m_aborted = m_yggdrasil->m_aborted; + + auto state = m_yggdrasil->taskState(); + QString errorMessage = tr("Custom user authentication failed."); + + // NOTE: soft error in the first step means 'offline' + if(state == AccountTaskState::STATE_FAILED_SOFT) { + state = AccountTaskState::STATE_OFFLINE; + errorMessage = tr("Custom user authentication ended with a network error. Is MutliFactor Auth current?"); + } + emit finished(state, errorMessage); +} diff --git a/launcher/minecraft/auth/steps/CustomStep.h b/launcher/minecraft/auth/steps/CustomStep.h new file mode 100644 index 00000000..bff05e44 --- /dev/null +++ b/launcher/minecraft/auth/steps/CustomStep.h @@ -0,0 +1,28 @@ +#pragma once +#include + +#include "QObjectPtr.h" +#include "minecraft/auth/AuthStep.h" + +class Yggdrasil; + +class CustomStep : public AuthStep { + Q_OBJECT + +public: + explicit CustomStep(AccountData *data, QString password, QString url); + virtual ~CustomStep() noexcept; + + void perform() override; + void rehydrate() override; + + QString describe() override; + +private slots: + void onAuthSucceeded(); + void onAuthFailed(); + +private: + Yggdrasil *m_yggdrasil = nullptr; + QString m_password; +}; diff --git a/launcher/minecraft/launch/InjectAuthlib.cpp b/launcher/minecraft/launch/InjectAuthlib.cpp index 06f31739..eef9f605 100644 --- a/launcher/minecraft/launch/InjectAuthlib.cpp +++ b/launcher/minecraft/launch/InjectAuthlib.cpp @@ -20,9 +20,10 @@ #include #include -InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector) : LaunchStep(parent) +InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector, QString url) : LaunchStep(parent) { m_injector = injector; + m_url = url; } void InjectAuthlib::executeTask() @@ -130,7 +131,7 @@ void InjectAuthlib::onVersionDownloadSucceeded() void InjectAuthlib::onDownloadSucceeded() { - QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg("ely.by"); + QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg(m_url); qDebug() << "Injecting " << injector; diff --git a/launcher/minecraft/launch/InjectAuthlib.h b/launcher/minecraft/launch/InjectAuthlib.h index 5274f55d..260b00df 100644 --- a/launcher/minecraft/launch/InjectAuthlib.h +++ b/launcher/minecraft/launch/InjectAuthlib.h @@ -71,5 +71,6 @@ private: bool m_offlineMode; QString m_versionName; QString m_authServer; + QString m_url; AuthlibInjectorPtr *m_injector; }; diff --git a/launcher/ui/dialogs/CustomLoginDialog.cpp b/launcher/ui/dialogs/CustomLoginDialog.cpp new file mode 100644 index 00000000..2789143b --- /dev/null +++ b/launcher/ui/dialogs/CustomLoginDialog.cpp @@ -0,0 +1,131 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "CustomLoginDialog.h" +#include "ui_CustomLoginDialog.h" + +#include "minecraft/auth/AccountTask.h" + +#include + +CustomLoginDialog::CustomLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::CustomLoginDialog) +{ + ui->setupUi(this); + ui->progressBar->setVisible(false); + ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); + + connect(ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); + connect(ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); +} + +CustomLoginDialog::~CustomLoginDialog() +{ + delete ui; +} + +// Stage 1: User interaction +void CustomLoginDialog::accept() +{ + setUserInputsEnabled(false); + ui->progressBar->setVisible(true); + + // Setup the login task and start it + m_account = MinecraftAccount::createCustom(ui->userTextBox->text()); + if (ui->mfaTextBox->text().length() > 0) { + m_loginTask = m_account->loginCustom(ui->passTextBox->text() + ':' + ui->mfaTextBox->text(), ui->urlTextBox->text()); + } + else { + m_loginTask = m_account->loginCustom(ui->passTextBox->text(), ui->urlTextBox->text()); + } + connect(m_loginTask.get(), &Task::failed, this, &CustomLoginDialog::onTaskFailed); + connect(m_loginTask.get(), &Task::succeeded, this, &CustomLoginDialog::onTaskSucceeded); + connect(m_loginTask.get(), &Task::status, this, &CustomLoginDialog::onTaskStatus); + connect(m_loginTask.get(), &Task::progress, this, &CustomLoginDialog::onTaskProgress); + m_loginTask->start(); +} + +void CustomLoginDialog::setUserInputsEnabled(bool enable) +{ + ui->userTextBox->setEnabled(enable); + ui->passTextBox->setEnabled(enable); + ui->urlTextBox->setEnabled(enable); + ui->mfaTextBox->setEnabled(enable); + ui->buttonBox->setEnabled(enable); +} + +// Enable the OK button only when both textboxes contain something. +void CustomLoginDialog::on_userTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty() && !ui->urlTextBox->text().isEmpty()); +} +void CustomLoginDialog::on_passTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty() && !ui->urlTextBox->text().isEmpty()); +} +void CustomLoginDialog::on_urlTextBox_textEdited(const QString &newText) +{ + ui->buttonBox->button(QDialogButtonBox::Ok) + ->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty() && !ui->passTextBox->text().isEmpty()); +} + +void CustomLoginDialog::onTaskFailed(const QString &reason) +{ + // Set message + auto lines = reason.split('\n'); + QString processed; + for(auto line: lines) { + if(line.size()) { + processed += "" + line + "
"; + } + else { + processed += "
"; + } + } + ui->label->setText(processed); + + // Re-enable user-interaction + setUserInputsEnabled(true); + ui->progressBar->setVisible(false); +} + +void CustomLoginDialog::onTaskSucceeded() +{ + QDialog::accept(); +} + +void CustomLoginDialog::onTaskStatus(const QString &status) +{ + ui->label->setText(status); +} + +void CustomLoginDialog::onTaskProgress(qint64 current, qint64 total) +{ + ui->progressBar->setMaximum(total); + ui->progressBar->setValue(current); +} + +// Public interface +MinecraftAccountPtr CustomLoginDialog::newAccount(QWidget *parent, QString msg) +{ + CustomLoginDialog dlg(parent); + dlg.ui->label->setText(msg); + if (dlg.exec() == QDialog::Accepted) + { + return dlg.m_account; + } + return nullptr; +} diff --git a/launcher/ui/dialogs/CustomLoginDialog.h b/launcher/ui/dialogs/CustomLoginDialog.h new file mode 100644 index 00000000..4be92c11 --- /dev/null +++ b/launcher/ui/dialogs/CustomLoginDialog.h @@ -0,0 +1,60 @@ +/* Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include +#include + +#include "minecraft/auth/MinecraftAccount.h" +#include "tasks/Task.h" + +namespace Ui +{ +class CustomLoginDialog; +} + +class CustomLoginDialog : public QDialog +{ + Q_OBJECT + +public: + ~CustomLoginDialog(); + + static MinecraftAccountPtr newAccount(QWidget *parent, QString message); + +private: + explicit CustomLoginDialog(QWidget *parent = 0); + + void setUserInputsEnabled(bool enable); + +protected +slots: + void accept(); + + void onTaskFailed(const QString &reason); + void onTaskSucceeded(); + void onTaskStatus(const QString &status); + void onTaskProgress(qint64 current, qint64 total); + + void on_userTextBox_textEdited(const QString &newText); + void on_passTextBox_textEdited(const QString &newText); + void on_urlTextBox_textEdited(const QString &newText); + +private: + Ui::CustomLoginDialog *ui; + MinecraftAccountPtr m_account; + Task::Ptr m_loginTask; +}; diff --git a/launcher/ui/dialogs/CustomLoginDialog.ui b/launcher/ui/dialogs/CustomLoginDialog.ui new file mode 100644 index 00000000..5a51e9da --- /dev/null +++ b/launcher/ui/dialogs/CustomLoginDialog.ui @@ -0,0 +1,94 @@ + + + CustomLoginDialog + + + + 0 + 0 + 421 + 198 + + + + + 0 + 0 + + + + Add Account + + + + + + Message label placeholder. + + + Qt::RichText + + + Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse + + + + + + + Email + + + + + + + QLineEdit::Password + + + Password + + + + + + + Url + + + + + + + QLineEdit::Password + + + 2FA Code (Optional) + + + + + + + 24 + + + false + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + diff --git a/launcher/ui/pages/global/AccountListPage.cpp b/launcher/ui/pages/global/AccountListPage.cpp index 134406f6..e3587dab 100644 --- a/launcher/ui/pages/global/AccountListPage.cpp +++ b/launcher/ui/pages/global/AccountListPage.cpp @@ -49,6 +49,7 @@ #include "ui/dialogs/LoginDialog.h" #include "ui/dialogs/MSALoginDialog.h" #include "ui/dialogs/ElybyLoginDialog.h" +#include "ui/dialogs/CustomLoginDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/SkinUploadDialog.h" @@ -219,6 +220,22 @@ void AccountListPage::on_actionAddElyby_triggered() } } +void AccountListPage::on_actionAddCustom_triggered() +{ + MinecraftAccountPtr account = CustomLoginDialog::newAccount( + this, + tr("Enter the custom yggdrasil server url, along with your username and password to add your account.") + ); + + if (account) + { + m_accounts->addAccount(account); + if (m_accounts->count() == 1) { + m_account->setDefaultAccount(account); + } + } +} + void AccountListPage::on_actionRemove_triggered() { QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); @@ -263,6 +280,7 @@ void AccountListPage::updateButtonStates() bool accountIsReady = false; bool accountIsOnline = false; bool accountIsElyby = false; + bool AccountIsCustom = false; if (hasSelection) { QModelIndex selected = selection.first(); @@ -270,12 +288,13 @@ void AccountListPage::updateButtonStates() accountIsReady = !account->isActive(); accountIsOnline = !account->isOffline(); accountIsElyby = account->isElyby(); + accountIsCustom = account->isCustom(); } ui->actionRemove->setEnabled(accountIsReady); ui->actionSetDefault->setEnabled(accountIsReady); - ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby); - ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby); + ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby && !accountIsCustom); + ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby && !accountIsCustom); ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline); if(m_accounts->defaultAccount().get() == nullptr) { diff --git a/launcher/ui/pages/global/AccountListPage.h b/launcher/ui/pages/global/AccountListPage.h index 1812c5e5..edd5b257 100644 --- a/launcher/ui/pages/global/AccountListPage.h +++ b/launcher/ui/pages/global/AccountListPage.h @@ -86,6 +86,7 @@ public slots: void on_actionAddMicrosoft_triggered(); void on_actionAddOffline_triggered(); void on_actionAddElyby_triggered(); + void on_actionAddCustom_triggered(); void on_actionRemove_triggered(); void on_actionRefresh_triggered(); void on_actionSetDefault_triggered(); diff --git a/launcher/ui/pages/global/AccountListPage.ui b/launcher/ui/pages/global/AccountListPage.ui index e5d76ec4..dd53c8c0 100644 --- a/launcher/ui/pages/global/AccountListPage.ui +++ b/launcher/ui/pages/global/AccountListPage.ui @@ -56,6 +56,7 @@ + @@ -115,6 +116,11 @@ Add &Ely.by + + + Add &Custom + + &Refresh