Add Ely.by accounts (#17)

* Initial Ely.by support

* Fix profile pictures for Ely.by

* Disable upload and delete skin buttons for Ely.by accounts

* Port UltimMC's authlib injector to PollyMC
This commit is contained in:
fn2006 2022-08-07 18:38:53 +01:00 committed by Fintan Martin
parent 050ee33132
commit 0170e8b177
26 changed files with 871 additions and 17 deletions

View File

@ -870,6 +870,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv)
m_metacache->addBase("translations", QDir("translations").absolutePath());
m_metacache->addBase("icons", QDir("cache/icons").absolutePath());
m_metacache->addBase("meta", QDir("meta").absolutePath());
m_metacache->addBase("injectors", QDir("injectors").absolutePath());
m_metacache->Load();
qDebug() << "<> Cache initialized.";
}

View File

@ -212,11 +212,15 @@ set(MINECRAFT_SOURCES
minecraft/auth/flows/MSA.h
minecraft/auth/flows/Offline.cpp
minecraft/auth/flows/Offline.h
minecraft/auth/flows/Elyby.cpp
minecraft/auth/flows/Elyby.h
minecraft/auth/steps/OfflineStep.cpp
minecraft/auth/steps/OfflineStep.h
minecraft/auth/steps/EntitlementsStep.cpp
minecraft/auth/steps/EntitlementsStep.h
minecraft/auth/steps/ElybyProfileStep.cpp
minecraft/auth/steps/ElybyProfileStep.h
minecraft/auth/steps/ElybyStep.cpp
minecraft/auth/steps/ElybyStep.h
minecraft/auth/steps/GetSkinStep.cpp
minecraft/auth/steps/GetSkinStep.h
minecraft/auth/steps/LauncherLoginStep.cpp
@ -229,6 +233,8 @@ set(MINECRAFT_SOURCES
minecraft/auth/steps/MinecraftProfileStepMojang.h
minecraft/auth/steps/MSAStep.cpp
minecraft/auth/steps/MSAStep.h
minecraft/auth/steps/OfflineStep.cpp
minecraft/auth/steps/OfflineStep.h
minecraft/auth/steps/XboxAuthorizationStep.cpp
minecraft/auth/steps/XboxAuthorizationStep.h
minecraft/auth/steps/XboxProfileStep.cpp
@ -264,6 +270,8 @@ set(MINECRAFT_SOURCES
minecraft/launch/LauncherPartLaunch.h
minecraft/launch/MinecraftServerTarget.cpp
minecraft/launch/MinecraftServerTarget.h
minecraft/launch/InjectAuthlib.cpp
minecraft/launch/InjectAuthlib.h
minecraft/launch/PrintInstanceInfo.cpp
minecraft/launch/PrintInstanceInfo.h
minecraft/launch/ReconstructAssets.cpp
@ -819,6 +827,8 @@ SET(LAUNCHER_SOURCES
ui/dialogs/CustomMessageBox.h
ui/dialogs/EditAccountDialog.cpp
ui/dialogs/EditAccountDialog.h
ui/dialogs/ElybyLoginDialog.cpp
ui/dialogs/ElybyLoginDialog.h
ui/dialogs/ExportInstanceDialog.cpp
ui/dialogs/ExportInstanceDialog.h
ui/dialogs/IconPickerDialog.cpp
@ -955,6 +965,7 @@ qt_wrap_ui(LAUNCHER_UI
ui/dialogs/IconPickerDialog.ui
ui/dialogs/MSALoginDialog.ui
ui/dialogs/OfflineLoginDialog.ui
ui/dialogs/ElybyLoginDialog.ui
ui/dialogs/AboutDialog.ui
ui/dialogs/LoginDialog.ui
ui/dialogs/EditAccountDialog.ui

View File

@ -348,6 +348,7 @@ QStringList MinecraftInstance::extraArguments() const
if (!addn.isEmpty()) {
list.append(addn);
}
auto agents = m_components->getProfile()->getAgents();
for (auto agent : agents)
{
@ -355,6 +356,13 @@ QStringList MinecraftInstance::extraArguments() const
agent->library()->getApplicableFiles(currentSystem, jar, temp1, temp2, temp3, getLocalLibraryPath());
list.append("-javaagent:"+jar[0]+(agent->argument().isEmpty() ? "" : "="+agent->argument()));
}
// TODO: figure out how polymc's javaagent system works and use it instead of this hack
if (m_injector) {
list.append("-javaagent:"+m_injector->javaArg);
list.append("-Dauthlibinjector.noShowServerName");
}
return list;
}
@ -972,7 +980,14 @@ shared_qobject_ptr<LaunchTask> MinecraftInstance::createLaunchTask(AuthSessionPt
if(!session->demo) {
process->appendStep(new ClaimAccount(pptr, session));
}
// authlib patch
if (session->user_type == "elyby")
{
process->appendStep(new InjectAuthlib(pptr, &m_injector));
}
process->appendStep(new Update(pptr, Net::Mode::Online));
}
else
{

View File

@ -5,6 +5,7 @@
#include <QProcess>
#include <QDir>
#include "minecraft/launch/MinecraftServerTarget.h"
#include "minecraft/launch/InjectAuthlib.h"
class ModFolderModel;
class WorldList;
@ -128,6 +129,7 @@ protected: // data
mutable std::shared_ptr<ModFolderModel> m_texture_pack_list;
mutable std::shared_ptr<WorldList> m_world_list;
mutable std::shared_ptr<GameOptions> m_game_options;
mutable std::shared_ptr<AuthlibInjector> m_injector;
};
typedef std::shared_ptr<MinecraftInstance> MinecraftInstancePtr;

View File

@ -352,6 +352,8 @@ bool AccountData::resumeStateFromV3(QJsonObject data) {
type = AccountType::Mojang;
} else if (typeS == "Offline") {
type = AccountType::Offline;
} else if (typeS == "Elyby") {
type = AccountType::Elyby;
} else {
qWarning() << "Failed to parse account data: type is not recognized.";
return false;
@ -409,6 +411,9 @@ QJsonObject AccountData::saveState() const {
else if (type == AccountType::Offline) {
output["type"] = "Offline";
}
else if (type == AccountType::Elyby) {
output["type"] = "Elyby";
}
tokenToJSONV3(output, yggdrasilToken, "ygg");
profileToJSONV3(output, minecraftProfile, "profile");
@ -428,14 +433,14 @@ QString AccountData::accessToken() const {
}
QString AccountData::clientToken() const {
if(type != AccountType::Mojang) {
if(type != AccountType::Mojang && type != AccountType::Elyby) {
return QString();
}
return yggdrasilToken.extra["clientToken"].toString();
}
void AccountData::setClientToken(QString clientToken) {
if(type != AccountType::Mojang) {
if(type != AccountType::Mojang && type != AccountType::Elyby) {
return;
}
yggdrasilToken.extra["clientToken"] = clientToken;
@ -449,7 +454,7 @@ void AccountData::generateClientTokenIfMissing() {
}
void AccountData::invalidateClientToken() {
if(type != AccountType::Mojang) {
if(type != AccountType::Mojang && type != AccountType::Elyby) {
return;
}
yggdrasilToken.extra["clientToken"] = QUuid::createUuid().toString().remove(QRegularExpression("[{-}]"));
@ -473,6 +478,9 @@ QString AccountData::accountDisplayString() const {
case AccountType::Mojang: {
return userName();
}
case AccountType::Elyby: {
return userName();
}
case AccountType::Offline: {
return QObject::tr("<Offline>");
}

View File

@ -74,7 +74,8 @@ struct MinecraftProfile {
enum class AccountType {
MSA,
Mojang,
Offline
Offline,
Elyby
};
enum class AccountState {

View File

@ -332,7 +332,7 @@ QVariant AccountList::data(const QModelIndex &index, int role) const
}
case MigrationColumn: {
if(account->isMSA() || account->isOffline()) {
if(!account->isMojang()) {
return tr("N/A", "Can Migrate?");
}
if (account->canMigrate()) {

View File

@ -51,6 +51,7 @@
#include "flows/MSA.h"
#include "flows/Mojang.h"
#include "flows/Offline.h"
#include "flows/Elyby.h"
MinecraftAccount::MinecraftAccount(QObject* parent) : QObject(parent) {
data.internalId = QUuid::createUuid().toString().remove(QRegularExpression("[{}-]"));
@ -106,6 +107,17 @@ MinecraftAccountPtr MinecraftAccount::createOffline(const QString &username)
return account;
}
MinecraftAccountPtr MinecraftAccount::createElyby(const QString &username)
{
MinecraftAccountPtr account = new MinecraftAccount();
account->data.type = AccountType::Elyby;
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
{
@ -162,6 +174,17 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::loginOffline() {
return m_currentTask;
}
shared_qobject_ptr<AccountTask> MinecraftAccount::loginElyby(QString password) {
Q_ASSERT(m_currentTask.get() == nullptr);
m_currentTask.reset(new ElybyLogin(&data, password));
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<AccountTask> MinecraftAccount::refresh() {
if(m_currentTask) {
return m_currentTask;
@ -173,6 +196,9 @@ shared_qobject_ptr<AccountTask> MinecraftAccount::refresh() {
else if(data.type == AccountType::Offline) {
m_currentTask.reset(new OfflineRefresh(&data));
}
else if(data.type == AccountType::Elyby) {
m_currentTask.reset(new ElybyRefresh(&data));
}
else {
m_currentTask.reset(new MojangRefresh(&data));
}

View File

@ -95,6 +95,8 @@ public: /* construction */
static MinecraftAccountPtr createOffline(const QString &username);
static MinecraftAccountPtr createElyby(const QString &username);
static MinecraftAccountPtr loadFromJsonV2(const QJsonObject &json);
static MinecraftAccountPtr loadFromJsonV3(const QJsonObject &json);
@ -113,6 +115,8 @@ public: /* manipulation */
shared_qobject_ptr<AccountTask> loginOffline();
shared_qobject_ptr<AccountTask> loginElyby(QString password);
shared_qobject_ptr<AccountTask> refresh();
shared_qobject_ptr<AccountTask> currentTask();
@ -152,10 +156,18 @@ public: /* queries */
return data.type == AccountType::MSA;
}
bool isMojang() const {
return data.type == AccountType::Mojang;
}
bool isOffline() const {
return data.type == AccountType::Offline;
}
bool isElyby() const {
return data.type == AccountType::Elyby;
}
bool ownsMinecraft() const {
return data.minecraftEntitlement.ownsMinecraft;
}
@ -180,6 +192,9 @@ public: /* queries */
case AccountType::Offline: {
return "offline";
}
case AccountType::Elyby: {
return "elyby";
}
break;
default: {
return "unknown";

View File

@ -55,7 +55,7 @@ void Yggdrasil::sendRequest(QUrl endpoint, QByteArray content) {
void Yggdrasil::executeTask() {
}
void Yggdrasil::refresh() {
void Yggdrasil::refresh(QString baseUrl) {
start();
/*
* {
@ -84,13 +84,13 @@ void Yggdrasil::refresh() {
req.insert("requestUser", false);
QJsonDocument doc(req);
QUrl reqUrl("https://authserver.mojang.com/refresh");
QUrl reqUrl(baseUrl + "refresh");
QByteArray requestData = doc.toJson();
sendRequest(reqUrl, requestData);
}
void Yggdrasil::login(QString password) {
void Yggdrasil::login(QString password, QString baseUrl) {
start();
/*
* {
@ -129,7 +129,7 @@ void Yggdrasil::login(QString password) {
QJsonDocument doc(req);
QUrl reqUrl("https://authserver.mojang.com/authenticate");
QUrl reqUrl(baseUrl + "authenticate");
QNetworkRequest netRequest(reqUrl);
QByteArray requestData = doc.toJson();

View File

@ -40,8 +40,8 @@ public:
);
virtual ~Yggdrasil() = default;
void refresh();
void login(QString password);
void refresh(QString baseUrl);
void login(QString password, QString baseUrl);
struct Error
{

View File

@ -0,0 +1,24 @@
#include "Elyby.h"
#include "minecraft/auth/steps/ElybyStep.h"
#include "minecraft/auth/steps/ElybyProfileStep.h"
#include "minecraft/auth/steps/GetSkinStep.h"
ElybyRefresh::ElybyRefresh(
AccountData *data,
QObject *parent
) : AuthFlow(data, parent) {
m_steps.append(new ElybyStep(m_data, QString()));
m_steps.append(new ElybyProfileStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}
ElybyLogin::ElybyLogin(
AccountData *data,
QString password,
QObject *parent
): AuthFlow(data, parent), m_password(password) {
m_steps.append(new ElybyStep(m_data, m_password));
m_steps.append(new ElybyProfileStep(m_data));
m_steps.append(new GetSkinStep(m_data));
}

View File

@ -0,0 +1,26 @@
#pragma once
#include "AuthFlow.h"
class ElybyRefresh : public AuthFlow
{
Q_OBJECT
public:
explicit ElybyRefresh(
AccountData *data,
QObject *parent = 0
);
};
class ElybyLogin : public AuthFlow
{
Q_OBJECT
public:
explicit ElybyLogin(
AccountData *data,
QString password,
QObject *parent = 0
);
private:
QString m_password;
};

View File

@ -0,0 +1,93 @@
#include "ElybyProfileStep.h"
#include <QNetworkRequest>
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "net/NetUtils.h"
ElybyProfileStep::ElybyProfileStep(AccountData* data) : AuthStep(data) {
}
ElybyProfileStep::~ElybyProfileStep() noexcept = default;
QString ElybyProfileStep::describe() {
return tr("Fetching the Minecraft profile.");
}
void ElybyProfileStep::perform() {
if (m_data->minecraftProfile.id.isEmpty()) {
emit finished(AccountTaskState::STATE_FAILED_HARD, tr("A UUID is required to get the profile."));
return;
}
QUrl url = QUrl("https://authserver.ely.by/session/profile/" + m_data->minecraftProfile.id);
QNetworkRequest req = QNetworkRequest(url);
AuthRequest *request = new AuthRequest(this);
connect(request, &AuthRequest::finished, this, &ElybyProfileStep::onRequestDone);
request->get(req);
}
void ElybyProfileStep::rehydrate() {
// NOOP, for now. We only save bools and there's nothing to check.
}
void ElybyProfileStep::onRequestDone(
QNetworkReply::NetworkError error,
QByteArray data,
QList<QNetworkReply::RawHeaderPair> headers
) {
auto requestor = qobject_cast<AuthRequest *>(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.")
);
}

View File

@ -0,0 +1,22 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class ElybyProfileStep : public AuthStep {
Q_OBJECT
public:
explicit ElybyProfileStep(AccountData *data);
virtual ~ElybyProfileStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onRequestDone(QNetworkReply::NetworkError, QByteArray, QList<QNetworkReply::RawHeaderPair>);
};

View File

@ -0,0 +1,52 @@
#include "ElybyStep.h"
#include "minecraft/auth/AuthRequest.h"
#include "minecraft/auth/Parsers.h"
#include "minecraft/auth/Yggdrasil.h"
ElybyStep::ElybyStep(AccountData* data, QString password) : AuthStep(data), m_password(password) {
m_yggdrasil = new Yggdrasil(m_data, this);
connect(m_yggdrasil, &Task::failed, this, &ElybyStep::onAuthFailed);
connect(m_yggdrasil, &Task::succeeded, this, &ElybyStep::onAuthSucceeded);
connect(m_yggdrasil, &Task::aborted, this, &ElybyStep::onAuthFailed);
}
ElybyStep::~ElybyStep() noexcept = default;
QString ElybyStep::describe() {
return tr("Logging in with Ely.by account.");
}
void ElybyStep::rehydrate() {
// NOOP, for now.
}
void ElybyStep::perform() {
if(m_password.size()) {
m_yggdrasil->login(m_password, "https://authserver.ely.by/auth/");
}
else {
m_yggdrasil->refresh("https://authserver.ely.by/auth/");
}
}
void ElybyStep::onAuthSucceeded() {
emit finished(AccountTaskState::STATE_WORKING, tr("Logged in with Ely.by"));
}
void ElybyStep::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("Ely.by user authentication failed.");
// NOTE: soft error in the first step means 'offline'
if(state == AccountTaskState::STATE_FAILED_SOFT) {
state = AccountTaskState::STATE_OFFLINE;
errorMessage = tr("Ely.by user authentication ended with a network error.");
}
emit finished(state, errorMessage);
}

View File

@ -0,0 +1,28 @@
#pragma once
#include <QObject>
#include "QObjectPtr.h"
#include "minecraft/auth/AuthStep.h"
class Yggdrasil;
class ElybyStep : public AuthStep {
Q_OBJECT
public:
explicit ElybyStep(AccountData *data, QString password);
virtual ~ElybyStep() noexcept;
void perform() override;
void rehydrate() override;
QString describe() override;
private slots:
void onAuthSucceeded();
void onAuthFailed();
private:
Yggdrasil *m_yggdrasil = nullptr;
QString m_password;
};

View File

@ -24,10 +24,10 @@ void YggdrasilStep::rehydrate() {
void YggdrasilStep::perform() {
if(m_password.size()) {
m_yggdrasil->login(m_password);
m_yggdrasil->login(m_password, "https://authserver.mojang.com/");
}
else {
m_yggdrasil->refresh();
m_yggdrasil->refresh("https://authserver.mojang.com/");
}
}

View File

@ -0,0 +1,173 @@
/* 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 "InjectAuthlib.h"
#include <launch/LaunchTask.h>
#include <minecraft/MinecraftInstance.h>
#include <FileSystem.h>
#include <Application.h>
#include <Json.h>
InjectAuthlib::InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr* injector) : LaunchStep(parent)
{
m_injector = injector;
}
void InjectAuthlib::executeTask()
{
if (m_aborted)
{
emitFailed(tr("Task aborted."));
return;
}
auto latestVersionInfo = QString("https://authlib-injector.yushi.moe/artifact/latest.json");
auto netJob = new NetJob("Injector versions info download", APPLICATION->network());
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", "version.json");
if (!m_offlineMode)
{
entry->setStale(true);
auto task = Net::Download::makeCached(QUrl(latestVersionInfo), entry);
netJob->addNetAction(task);
jobPtr.reset(netJob);
QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onVersionDownloadSucceeded);
QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed);
jobPtr->start();
}
else
{
onVersionDownloadSucceeded();
}
}
void InjectAuthlib::onVersionDownloadSucceeded()
{
QByteArray data;
try
{
data = FS::read(QDir("injectors").absoluteFilePath("version.json"));
}
catch (const Exception &e)
{
qCritical() << "Translations Download Failed: index file not readable";
jobPtr.reset();
emitFailed("Error while parsing JSON response from InjectorEndpoint");
return;
}
QJsonParseError parse_error;
QJsonDocument doc = QJsonDocument::fromJson(data, &parse_error);
if (parse_error.error != QJsonParseError::NoError)
{
qCritical() << "Error while parsing JSON response from InjectorEndpoint at " << parse_error.offset << " reason: " << parse_error.errorString();
qCritical() << data;
jobPtr.reset();
emitFailed("Error while parsing JSON response from InjectorEndpoint");
return;
}
if (!doc.isObject())
{
qCritical() << "Error while parsing JSON response from InjectorEndpoint root is not object";
qCritical() << data;
jobPtr.reset();
emitFailed("Error while parsing JSON response from InjectorEndpoint");
return;
}
QString downloadUrl;
try
{
downloadUrl = Json::requireString(doc.object(), "download_url");
}
catch (const JSONValidationError &e)
{
qCritical() << "Error while parsing JSON response from InjectorEndpoint download url is not string";
qCritical() << e.cause();
qCritical() << data;
jobPtr.reset();
emitFailed("Error while parsing JSON response from InjectorEndpoint");
return;
}
QFileInfo fi(downloadUrl);
m_versionName = fi.fileName();
qDebug() << "Authlib injector version:" << m_versionName;
if (!m_offlineMode)
{
auto netJob = new NetJob("Injector download", APPLICATION->network());
MetaEntryPtr entry = APPLICATION->metacache()->resolveEntry("injectors", m_versionName);
entry->setStale(true);
auto task = Net::Download::makeCached(QUrl(downloadUrl), entry);
netJob->addNetAction(task);
jobPtr.reset(netJob);
QObject::connect(netJob, &NetJob::succeeded, this, &InjectAuthlib::onDownloadSucceeded);
QObject::connect(netJob, &NetJob::failed, this, &InjectAuthlib::onDownloadFailed);
jobPtr->start();
}
else
{
onDownloadSucceeded();
}
}
void InjectAuthlib::onDownloadSucceeded()
{
QString injector = QString("%1=%2").arg(QDir("injectors").absoluteFilePath(m_versionName)).arg("ely.by");
qDebug()
<< "Injecting " << injector;
auto inj = new AuthlibInjector(injector);
m_injector->reset(inj);
jobPtr.reset();
emitSucceeded();
}
void InjectAuthlib::onDownloadFailed(QString reason)
{
jobPtr.reset();
emitFailed(reason);
}
void InjectAuthlib::proceed()
{
}
bool InjectAuthlib::canAbort() const
{
if (jobPtr)
{
return jobPtr->canAbort();
}
return true;
}
bool InjectAuthlib::abort()
{
m_aborted = true;
if (jobPtr)
{
if (jobPtr->canAbort())
{
return jobPtr->abort();
}
}
return true;
}

View File

@ -0,0 +1,75 @@
/* 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 <launch/LaunchStep.h>
#include <QObjectPtr.h>
#include <LoggedProcess.h>
#include <java/JavaChecker.h>
#include <net/Mode.h>
#include <net/NetJob.h>
struct AuthlibInjector
{
QString javaArg;
AuthlibInjector(const QString arg)
{
javaArg = std::move(arg);
qDebug() << "NEW INJECTOR" << javaArg;
}
};
typedef std::shared_ptr<AuthlibInjector> AuthlibInjectorPtr;
// FIXME: stupid. should be defined by the instance type? or even completely abstracted away...
class InjectAuthlib : public LaunchStep
{
Q_OBJECT
public:
InjectAuthlib(LaunchTask *parent, AuthlibInjectorPtr *injector);
virtual ~InjectAuthlib(){};
void executeTask() override;
bool canAbort() const override;
void proceed() override;
void setAuthServer(QString server)
{
m_authServer = server;
};
void setOfflineMode(bool offline) {
m_offlineMode = offline;
}
public slots:
bool abort() override;
private slots:
void onVersionDownloadSucceeded();
void onDownloadSucceeded();
void onDownloadFailed(QString reason);
private:
shared_qobject_ptr<NetJob> jobPtr;
bool m_aborted = false;
bool m_offlineMode;
QString m_versionName;
QString m_authServer;
AuthlibInjectorPtr *m_injector;
};

View File

@ -0,0 +1,119 @@
/* 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 "ElybyLoginDialog.h"
#include "ui_ElybyLoginDialog.h"
#include "minecraft/auth/AccountTask.h"
#include <QtWidgets/QPushButton>
ElybyLoginDialog::ElybyLoginDialog(QWidget *parent) : QDialog(parent), ui(new Ui::ElybyLoginDialog)
{
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);
}
ElybyLoginDialog::~ElybyLoginDialog()
{
delete ui;
}
// Stage 1: User interaction
void ElybyLoginDialog::accept()
{
setUserInputsEnabled(false);
ui->progressBar->setVisible(true);
// Setup the login task and start it
m_account = MinecraftAccount::createElyby(ui->userTextBox->text());
m_loginTask = m_account->loginElyby(ui->passTextBox->text());
connect(m_loginTask.get(), &Task::failed, this, &ElybyLoginDialog::onTaskFailed);
connect(m_loginTask.get(), &Task::succeeded, this, &ElybyLoginDialog::onTaskSucceeded);
connect(m_loginTask.get(), &Task::status, this, &ElybyLoginDialog::onTaskStatus);
connect(m_loginTask.get(), &Task::progress, this, &ElybyLoginDialog::onTaskProgress);
m_loginTask->start();
}
void ElybyLoginDialog::setUserInputsEnabled(bool enable)
{
ui->userTextBox->setEnabled(enable);
ui->passTextBox->setEnabled(enable);
ui->buttonBox->setEnabled(enable);
}
// Enable the OK button only when both textboxes contain something.
void ElybyLoginDialog::on_userTextBox_textEdited(const QString &newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!newText.isEmpty() && !ui->passTextBox->text().isEmpty());
}
void ElybyLoginDialog::on_passTextBox_textEdited(const QString &newText)
{
ui->buttonBox->button(QDialogButtonBox::Ok)
->setEnabled(!newText.isEmpty() && !ui->userTextBox->text().isEmpty());
}
void ElybyLoginDialog::onTaskFailed(const QString &reason)
{
// Set message
auto lines = reason.split('\n');
QString processed;
for(auto line: lines) {
if(line.size()) {
processed += "<font color='red'>" + line + "</font><br />";
}
else {
processed += "<br />";
}
}
ui->label->setText(processed);
// Re-enable user-interaction
setUserInputsEnabled(true);
ui->progressBar->setVisible(false);
}
void ElybyLoginDialog::onTaskSucceeded()
{
QDialog::accept();
}
void ElybyLoginDialog::onTaskStatus(const QString &status)
{
ui->label->setText(status);
}
void ElybyLoginDialog::onTaskProgress(qint64 current, qint64 total)
{
ui->progressBar->setMaximum(total);
ui->progressBar->setValue(current);
}
// Public interface
MinecraftAccountPtr ElybyLoginDialog::newAccount(QWidget *parent, QString msg)
{
ElybyLoginDialog dlg(parent);
dlg.ui->label->setText(msg);
if (dlg.exec() == QDialog::Accepted)
{
return dlg.m_account;
}
return 0;
}

View File

@ -0,0 +1,59 @@
/* 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 <QtWidgets/QDialog>
#include <QtCore/QEventLoop>
#include "minecraft/auth/MinecraftAccount.h"
#include "tasks/Task.h"
namespace Ui
{
class ElybyLoginDialog;
}
class ElybyLoginDialog : public QDialog
{
Q_OBJECT
public:
~ElybyLoginDialog();
static MinecraftAccountPtr newAccount(QWidget *parent, QString message);
private:
explicit ElybyLoginDialog(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);
private:
Ui::ElybyLoginDialog *ui;
MinecraftAccountPtr m_account;
Task::Ptr m_loginTask;
};

View File

@ -0,0 +1,77 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>ElybyLoginDialog</class>
<widget class="QDialog" name="ElybyLoginDialog">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>421</width>
<height>198</height>
</rect>
</property>
<property name="sizePolicy">
<sizepolicy hsizetype="Fixed" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="windowTitle">
<string>Add Account</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<widget class="QLabel" name="label">
<property name="text">
<string notr="true">Message label placeholder.</string>
</property>
<property name="textFormat">
<enum>Qt::RichText</enum>
</property>
<property name="textInteractionFlags">
<set>Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse</set>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="userTextBox">
<property name="placeholderText">
<string>Email</string>
</property>
</widget>
</item>
<item>
<widget class="QLineEdit" name="passTextBox">
<property name="echoMode">
<enum>QLineEdit::Password</enum>
</property>
<property name="placeholderText">
<string>Password</string>
</property>
</widget>
</item>
<item>
<widget class="QProgressBar" name="progressBar">
<property name="value">
<number>24</number>
</property>
<property name="textVisible">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QDialogButtonBox" name="buttonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -48,6 +48,7 @@
#include "ui/dialogs/OfflineLoginDialog.h"
#include "ui/dialogs/LoginDialog.h"
#include "ui/dialogs/MSALoginDialog.h"
#include "ui/dialogs/ElybyLoginDialog.h"
#include "ui/dialogs/CustomMessageBox.h"
#include "ui/dialogs/SkinUploadDialog.h"
@ -202,6 +203,22 @@ void AccountListPage::on_actionAddOffline_triggered()
}
}
void AccountListPage::on_actionAddElyby_triggered()
{
MinecraftAccountPtr account = ElybyLoginDialog::newAccount(
this,
tr("Please enter your Ely.by account email and password to add your account.")
);
if (account)
{
m_accounts->addAccount(account);
if (m_accounts->count() == 1) {
m_accounts->setDefaultAccount(account);
}
}
}
void AccountListPage::on_actionRemove_triggered()
{
QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes();
@ -245,17 +262,20 @@ void AccountListPage::updateButtonStates()
bool hasSelection = !selection.empty();
bool accountIsReady = false;
bool accountIsOnline = false;
bool accountIsElyby = false;
if (hasSelection)
{
QModelIndex selected = selection.first();
MinecraftAccountPtr account = selected.data(AccountList::PointerRole).value<MinecraftAccountPtr>();
accountIsReady = !account->isActive();
accountIsOnline = !account->isOffline();
accountIsElyby = account->isElyby();
}
ui->actionRemove->setEnabled(accountIsReady);
ui->actionSetDefault->setEnabled(accountIsReady);
ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline);
ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline);
ui->actionUploadSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby);
ui->actionDeleteSkin->setEnabled(accountIsReady && accountIsOnline && !accountIsElyby);
ui->actionRefresh->setEnabled(accountIsReady && accountIsOnline);
if(m_accounts->defaultAccount().get() == nullptr) {

View File

@ -85,6 +85,7 @@ public slots:
void on_actionAddMojang_triggered();
void on_actionAddMicrosoft_triggered();
void on_actionAddOffline_triggered();
void on_actionAddElyby_triggered();
void on_actionRemove_triggered();
void on_actionRefresh_triggered();
void on_actionSetDefault_triggered();

View File

@ -55,6 +55,7 @@
<addaction name="actionAddMicrosoft"/>
<addaction name="actionAddMojang"/>
<addaction name="actionAddOffline"/>
<addaction name="actionAddElyby"/>
<addaction name="actionRefresh"/>
<addaction name="actionRemove"/>
<addaction name="actionSetDefault"/>
@ -109,6 +110,11 @@
<string>Add &amp;Offline</string>
</property>
</action>
<action name="actionAddElyby">
<property name="text">
<string>Add &amp;Ely.by</string>
</property>
</action>
<action name="actionRefresh">
<property name="text">
<string>&amp;Refresh</string>