diff --git a/.gitignore b/.gitignore index 0af63a46..597bbbec 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,11 @@ build resources/CMakeFiles resources/MultiMCLauncher.jar *~ +*.swp + +# Ctags File +tags + +# YouCompleteMe config stuff. +.ycm_extra_conf.* + diff --git a/CMakeLists.txt b/CMakeLists.txt index a9de11d1..1bb3126b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -217,6 +217,8 @@ gui/dialogs/EditNotesDialog.h gui/dialogs/EditNotesDialog.cpp gui/dialogs/CustomMessageBox.h gui/dialogs/CustomMessageBox.cpp +gui/dialogs/AccountListDialog.h +gui/dialogs/AccountListDialog.cpp # GUI - widgets gui/widgets/InstanceDelegate.h @@ -269,6 +271,14 @@ logic/net/LoginTask.cpp logic/net/S3ListBucket.h logic/net/S3ListBucket.cpp +# Yggdrasil login stuff +logic/auth/MojangAccount.h +logic/auth/MojangAccount.cpp +logic/auth/YggdrasilTask.h +logic/auth/YggdrasilTask.cpp +logic/auth/AuthenticateTask.h +logic/auth/AuthenticateTask.cpp + # legacy instances logic/LegacyInstance.h @@ -317,6 +327,8 @@ logic/lists/ForgeVersionList.h logic/lists/ForgeVersionList.cpp logic/lists/JavaVersionList.h logic/lists/JavaVersionList.cpp +logic/lists/MojangAccountList.h +logic/lists/MojangAccountList.cpp # misc model/view logic/EnabledItemFilter.h @@ -359,6 +371,7 @@ gui/dialogs/IconPickerDialog.ui gui/dialogs/LegacyModEditDialog.ui gui/dialogs/OneSixModEditDialog.ui gui/dialogs/EditNotesDialog.ui +gui/dialogs/AccountListDialog.ui # Widgets/other gui/widgets/MCModInfoFrame.ui diff --git a/MultiMC.cpp b/MultiMC.cpp index 0aaf31bf..17981755 100644 --- a/MultiMC.cpp +++ b/MultiMC.cpp @@ -10,6 +10,7 @@ #include "gui/MainWindow.h" #include "gui/dialogs/VersionSelectDialog.h" #include "logic/lists/InstanceList.h" +#include "logic/lists/MojangAccountList.h" #include "logic/lists/IconList.h" #include "logic/lists/LwjglVersionList.h" #include "logic/lists/MinecraftVersionList.h" @@ -146,6 +147,12 @@ MultiMC::MultiMC(int &argc, char **argv) : QApplication(argc, argv) connect(InstDirSetting, SIGNAL(settingChanged(const Setting &, QVariant)), m_instances.get(), SLOT(on_InstFolderChanged(const Setting &, QVariant))); + // and accounts + m_accounts.reset(new MojangAccountList(this)); + QLOG_INFO() << "Loading accounts..."; + m_accounts->setListFilePath("accounts.json", true); + m_accounts->loadList(); + // init the http meta cache initHttpMetaCache(); diff --git a/MultiMC.h b/MultiMC.h index cd4a5f7d..dba923b1 100644 --- a/MultiMC.h +++ b/MultiMC.h @@ -13,6 +13,7 @@ class LWJGLVersionList; class HttpMetaCache; class SettingsObject; class InstanceList; +class MojangAccountList; class IconList; class QNetworkAccessManager; class ForgeVersionList; @@ -57,6 +58,11 @@ public: return m_instances; } + std::shared_ptr accounts() + { + return m_accounts; + } + std::shared_ptr icons(); Status status() @@ -101,6 +107,7 @@ private: std::shared_ptr m_mmc_translator; std::shared_ptr m_settings; std::shared_ptr m_instances; + std::shared_ptr m_accounts; std::shared_ptr m_icons; std::shared_ptr m_qnam; std::shared_ptr m_metacache; diff --git a/gui/MainWindow.cpp b/gui/MainWindow.cpp index b8766d9a..6ae41f50 100644 --- a/gui/MainWindow.cpp +++ b/gui/MainWindow.cpp @@ -57,6 +57,7 @@ #include "gui/dialogs/IconPickerDialog.h" #include "gui/dialogs/EditNotesDialog.h" #include "gui/dialogs/CopyInstanceDialog.h" +#include "gui/dialogs/AccountListDialog.h" #include "gui/ConsoleWindow.h" @@ -84,11 +85,6 @@ MainWindow::MainWindow(QWidget *parent) : QMainWindow(parent), ui(new Ui::MainWi ui->setupUi(this); setWindowTitle(QString("MultiMC %1").arg(MMC->version().toString())); - // Set the selected instance to null - m_selectedInstance = nullptr; - // Set active instance to null. - m_activeInst = nullptr; - // OSX magic. setUnifiedTitleAndToolBarOnMac(true); @@ -428,6 +424,12 @@ void MainWindow::on_actionSettings_triggered() proxymodel->sort(0); } +void MainWindow::on_actionManageAccounts_triggered() +{ + AccountListDialog dialog(this); + dialog.exec(); +} + void MainWindow::on_actionReportBug_triggered() { openWebPage(QUrl("http://multimc.myjetbrains.com/youtrack/dashboard#newissue=yes")); @@ -538,11 +540,7 @@ void MainWindow::instanceActivated(QModelIndex index) NagUtils::checkJVMArgs(inst->settings().get("JvmArgs").toString(), this); - bool autoLogin = inst->settings().get("AutoLogin").toBool(); - if (autoLogin) - doAutoLogin(); - else - doLogin(); + doLogin(); } void MainWindow::on_actionLaunchInstance_triggered() @@ -554,106 +552,74 @@ void MainWindow::on_actionLaunchInstance_triggered() } } -void MainWindow::doAutoLogin() -{ - if (!m_selectedInstance) - return; - - Keyring *k = Keyring::instance(); - QStringList accounts = k->getStoredAccounts("minecraft"); - - if (!accounts.isEmpty()) - { - QString username = accounts[0]; - QString password = k->getPassword("minecraft", username); - - if (!password.isEmpty()) - { - QLOG_INFO() << "Automatically logging in with stored account: " << username; - m_activeInst = m_selectedInstance; - doLogin(username, password); - } - else - { - QLOG_ERROR() << "Auto login set for account, but no password was found: " - << username; - doLogin(tr("Auto login attempted, but no password is stored.")); - } - } - else - { - QLOG_ERROR() << "Auto login set but no accounts were stored."; - doLogin(tr("Auto login attempted, but no accounts are stored.")); - } -} - -void MainWindow::doLogin(QString username, QString password) -{ - PasswordLogin uInfo{username, password}; - - ProgressDialog *tDialog = new ProgressDialog(this); - LoginTask *loginTask = new LoginTask(uInfo, tDialog); - connect(loginTask, SIGNAL(succeeded()), SLOT(onLoginComplete()), Qt::QueuedConnection); - connect(loginTask, SIGNAL(failed(QString)), SLOT(doLogin(QString)), Qt::QueuedConnection); - - tDialog->exec(loginTask); -} - void MainWindow::doLogin(const QString &errorMsg) { if (!m_selectedInstance) return; - LoginDialog *loginDlg = new LoginDialog(this, errorMsg); - if (!m_selectedInstance->lastLaunch()) - loginDlg->forceOnline(); - - loginDlg->exec(); - if (loginDlg->result() == QDialog::Accepted) + // Find an account to use. + std::shared_ptr accounts = MMC->accounts(); + MojangAccountPtr account = accounts->activeAccount(); + if (accounts->count() <= 0) { - if (loginDlg->isOnline()) + // Tell the user they need to log in at least one account in order to play. + auto reply = CustomMessageBox::selectable(this, tr("No Accounts"), + tr("In order to play Minecraft, you must have at least one Mojang or Minecraft account logged in to MultiMC." + "Would you like to open the account manager to add an account now?"), + QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec(); + + if (reply == QMessageBox::Yes) { - m_activeInst = m_selectedInstance; - doLogin(loginDlg->getUsername(), loginDlg->getPassword()); + // Open the account manager. + on_actionManageAccounts_triggered(); } - else + } + else if (account.get() == nullptr) + { + // Tell the user they need to log in at least one account in order to play. + auto reply = CustomMessageBox::selectable(this, tr("No Account Selected"), + tr("You don't have an account selected as an active account." + "Would you like to open the account manager to select one now?"), + QMessageBox::Information, QMessageBox::Yes | QMessageBox::No)->exec(); + + if (reply == QMessageBox::Yes) { - QString user = loginDlg->getUsername(); - if (user.length() == 0) - user = QString("Player"); - m_activeLogin = {user, QString("Offline"), user, QString()}; - m_activeInst = m_selectedInstance; - launchInstance(m_activeInst, m_activeLogin); + // Open the account manager. + on_actionManageAccounts_triggered(); } } + else + { + // We'll need to validate the access token to make sure the account is still logged in. + // TODO: Do that ^ + + prepareLaunch(m_selectedInstance, account); + } } -void MainWindow::onLoginComplete() +void MainWindow::prepareLaunch(BaseInstance* instance, MojangAccountPtr account) { - if (!m_activeInst) - return; - LoginTask *task = (LoginTask *)QObject::sender(); - m_activeLogin = task->getResult(); - - Task *updateTask = m_activeInst->doUpdate(); + Task *updateTask = instance->doUpdate(); if (!updateTask) { - launchInstance(m_activeInst, m_activeLogin); + launchInstance(instance, account); } else { ProgressDialog tDialog(this); - connect(updateTask, SIGNAL(succeeded()), SLOT(onGameUpdateComplete())); + connect(updateTask, &Task::succeeded, [this, instance, account] { launchInstance(instance, account); }); connect(updateTask, SIGNAL(failed(QString)), SLOT(onGameUpdateError(QString))); tDialog.exec(updateTask); delete updateTask; } - auto job = new NetJob("Player skin: " + m_activeLogin.player_name); + QString playerName = account->currentProfile()->name(); - auto meta = MMC->metacache()->resolveEntry("skins", m_activeLogin.player_name + ".png"); + auto job = new NetJob("Player skin: " + playerName); + + auto meta = MMC->metacache()->resolveEntry("skins", playerName + ".png"); auto action = CacheDownload::make( - QUrl("http://skins.minecraft.net/MinecraftSkins/" + m_activeLogin.player_name + ".png"), + QUrl("http://skins.minecraft.net/MinecraftSkins/" + playerName + ".png"), meta); job->addNetAction(action); meta->stale = true; @@ -678,12 +644,12 @@ void MainWindow::onLoginComplete() QJsonDocument jsonDoc = QJsonDocument::fromJson(data, &jsonError); QJsonObject root = jsonDoc.object(); QJsonObject mappings = root.value("mappings").toObject(); - QJsonArray usernames = mappings.value(m_activeLogin.username).toArray(); + QJsonArray usernames = mappings.value(account->username()).toArray(); - if (!usernames.contains(m_activeLogin.player_name)) + if (!usernames.contains(playerName)) { - usernames.prepend(m_activeLogin.player_name); - mappings[m_activeLogin.username] = usernames; + usernames.prepend(playerName); + mappings[account->username()] = usernames; root["mappings"] = mappings; jsonDoc.setObject(root); @@ -693,22 +659,12 @@ void MainWindow::onLoginComplete() } } -void MainWindow::onGameUpdateComplete() -{ - launchInstance(m_activeInst, m_activeLogin); -} - -void MainWindow::onGameUpdateError(QString error) -{ - CustomMessageBox::selectable(this, tr("Error updating instance"), error, - QMessageBox::Warning)->show(); -} - -void MainWindow::launchInstance(BaseInstance *instance, LoginResponse response) +void MainWindow::launchInstance(BaseInstance *instance, MojangAccountPtr account) { Q_ASSERT_X(instance != NULL, "launchInstance", "instance is NULL"); + Q_ASSERT_X(account.get() != nullptr, "launchInstance", "account is NULL"); - proc = instance->prepareForLaunch(response); + proc = instance->prepareForLaunch(account); if (!proc) return; @@ -717,10 +673,19 @@ void MainWindow::launchInstance(BaseInstance *instance, LoginResponse response) console = new ConsoleWindow(proc); connect(console, SIGNAL(isClosing()), this, SLOT(instanceEnded())); - proc->setLogin(response.username, response.session_id); + // I think this will work... + QString username = account->username(); + QString session_id = account->accessToken(); + proc->setLogin(username, session_id); proc->launch(); } +void MainWindow::onGameUpdateError(QString error) +{ + CustomMessageBox::selectable(this, tr("Error updating instance"), error, + QMessageBox::Warning)->show(); +} + void MainWindow::taskStart() { // Nothing to do here yet. diff --git a/gui/MainWindow.h b/gui/MainWindow.h index b1678f76..3b8d4668 100644 --- a/gui/MainWindow.h +++ b/gui/MainWindow.h @@ -22,6 +22,8 @@ #include "logic/net/LoginTask.h" #include "logic/BaseInstance.h" +#include "logic/auth/MojangAccount.h" + class QToolButton; class LabeledToolButton; class QLabel; @@ -80,6 +82,8 @@ slots: void on_actionSettings_triggered(); + void on_actionManageAccounts_triggered(); + void on_actionReportBug_triggered(); void on_actionNews_triggered(); @@ -103,12 +107,18 @@ slots: void on_actionEditInstNotes_triggered(); void doLogin(const QString &errorMsg = ""); - void doLogin(QString username, QString password); - void doAutoLogin(); - void onLoginComplete(); + /*! + * Launches the given instance with the given account. + * This function assumes that the given account has a valid, usable access token. + */ + void launchInstance(BaseInstance* instance, MojangAccountPtr account); + + /*! + * Prepares the given instance for launch with the given account. + */ + void prepareLaunch(BaseInstance* instance, MojangAccountPtr account); - void onGameUpdateComplete(); void onGameUpdateError(QString error); void taskStart(); @@ -136,8 +146,6 @@ slots: void startTask(Task *task); - void launchInstance(BaseInstance *inst, LoginResponse response); - protected: bool eventFilter(QObject *obj, QEvent *ev); void setCatBackground(bool enabled); @@ -155,12 +163,6 @@ private: BaseInstance *m_selectedInstance; - // A pointer to the instance we are actively doing stuff with. - // This is set when the user launches an instance and is used to refer to that - // instance throughout the launching process. - BaseInstance *m_activeInst; - LoginResponse m_activeLogin; - Task *m_versionLoadTask; QLabel *m_statusLeft; diff --git a/gui/MainWindow.ui b/gui/MainWindow.ui index 6f70fc98..f76d4d4e 100644 --- a/gui/MainWindow.ui +++ b/gui/MainWindow.ui @@ -70,6 +70,7 @@ + @@ -465,6 +466,14 @@ Add a new instance. + + + Manage Accounts + + + Manage your Mojang or Minecraft accounts. + + diff --git a/gui/dialogs/AccountListDialog.cpp b/gui/dialogs/AccountListDialog.cpp new file mode 100644 index 00000000..c685c164 --- /dev/null +++ b/gui/dialogs/AccountListDialog.cpp @@ -0,0 +1,112 @@ +/* Copyright 2013 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 "AccountListDialog.h" +#include "ui_AccountListDialog.h" + +#include + +#include + +#include + +#include +#include + +#include + +AccountListDialog::AccountListDialog(QWidget *parent) : + QDialog(parent), + ui(new Ui::AccountListDialog) +{ + ui->setupUi(this); + + m_accounts = MMC->accounts(); + // TODO: Make the "Active?" column show checkboxes or radio buttons. + ui->listView->setModel(m_accounts.get()); +} + +AccountListDialog::~AccountListDialog() +{ + delete ui; +} + + +void AccountListDialog::on_addAccountBtn_clicked() +{ + doLogin("Please log in to add your account."); +} + +void AccountListDialog::on_rmAccountBtn_clicked() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + m_accounts->removeAccount(selected); + } +} + +void AccountListDialog::on_editAccountBtn_clicked() +{ + // TODO +} + +void AccountListDialog::on_setActiveBtn_clicked() +{ + QModelIndexList selection = ui->listView->selectionModel()->selectedIndexes(); + if (selection.size() > 0) + { + QModelIndex selected = selection.first(); + MojangAccountPtr account = selected.data(MojangAccountList::PointerRole).value(); + m_accounts->setActiveAccount(account->username()); + } +} + +void AccountListDialog::on_closeBtnBox_rejected() +{ + close(); +} + +void AccountListDialog::doLogin(const QString& errMsg) +{ + // TODO: We can use the login dialog for this for now, but we'll have to make something better for it eventually. + LoginDialog loginDialog(this); + loginDialog.exec(); + + if (loginDialog.result() == QDialog::Accepted) + { + QString username(loginDialog.getUsername()); + QString password(loginDialog.getPassword()); + + MojangAccountPtr account = MojangAccountPtr(new MojangAccount(username)); + + ProgressDialog* progDialog = new ProgressDialog(this); + m_authTask = new AuthenticateTask(account, password, progDialog); + connect(m_authTask, SIGNAL(succeeded()), SLOT(onLoginComplete()), Qt::QueuedConnection); + connect(m_authTask, SIGNAL(failed(QString)), SLOT(doLogin(QString)), Qt::QueuedConnection); + progDialog->exec(m_authTask); + //delete m_authTask; + } +} + +void AccountListDialog::onLoginComplete() +{ + // Add the authenticated account to the accounts list. + MojangAccountPtr account = m_authTask->getMojangAccount(); + m_accounts->addAccount(account); + //ui->listView->update(); +} + diff --git a/gui/dialogs/AccountListDialog.h b/gui/dialogs/AccountListDialog.h new file mode 100644 index 00000000..17a50bec --- /dev/null +++ b/gui/dialogs/AccountListDialog.h @@ -0,0 +1,63 @@ +/* Copyright 2013 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 "logic/lists/MojangAccountList.h" + +namespace Ui { +class AccountListDialog; +} + +class AuthenticateTask; + +class AccountListDialog : public QDialog +{ +Q_OBJECT +public: + explicit AccountListDialog(QWidget *parent = 0); + ~AccountListDialog(); + +public +slots: + void on_addAccountBtn_clicked(); + + void on_rmAccountBtn_clicked(); + + void on_editAccountBtn_clicked(); + + void on_setActiveBtn_clicked(); + + // This will be sent when the "close" button is clicked. + void on_closeBtnBox_rejected(); + +protected: + std::shared_ptr m_accounts; + + AuthenticateTask* m_authTask; + +protected +slots: + void doLogin(const QString& errMsg=""); + void onLoginComplete(); + +private: + Ui::AccountListDialog *ui; +}; + diff --git a/gui/dialogs/AccountListDialog.ui b/gui/dialogs/AccountListDialog.ui new file mode 100644 index 00000000..2872b368 --- /dev/null +++ b/gui/dialogs/AccountListDialog.ui @@ -0,0 +1,93 @@ + + + AccountListDialog + + + + 0 + 0 + 400 + 300 + + + + Dialog + + + + + + <html><head/><body><p>Welcome! If you're new here, you can click the &quot;Add&quot; button to add your Mojang or Minecraft account.</p></body></html> + + + true + + + + + + + + + + + + + + &Add + + + + + + + &Edit + + + + + + + &Remove + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + <html><head/><body><p>Set the currently selected account as the active account. The active account is the account that is used to log in (unless it is overridden in an instance-specific setting).</p></body></html> + + + &Set Active + + + + + + + + + + + QDialogButtonBox::Close + + + + + + + + diff --git a/logic/BaseInstance.h b/logic/BaseInstance.h index 6c9b16c7..cf86fda6 100644 --- a/logic/BaseInstance.h +++ b/logic/BaseInstance.h @@ -22,7 +22,7 @@ #include "inifile.h" #include "lists/BaseVersionList.h" -#include "net/LoginTask.h" +#include "logic/auth/MojangAccount.h" class QDialog; class Task; @@ -153,8 +153,8 @@ public: /// returns a valid update task if update is needed, NULL otherwise virtual Task *doUpdate() = 0; - /// returns a valid minecraft process, ready for launch - virtual MinecraftProcess *prepareForLaunch(LoginResponse response) = 0; + /// returns a valid minecraft process, ready for launch with the given account. + virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account) = 0; /// do any necessary cleanups after the instance finishes. also runs before /// 'prepareForLaunch' diff --git a/logic/InstanceLauncher.cpp b/logic/InstanceLauncher.cpp index 534d07d1..0ef0f045 100644 --- a/logic/InstanceLauncher.cpp +++ b/logic/InstanceLauncher.cpp @@ -38,6 +38,8 @@ void InstanceLauncher::onTerminated() void InstanceLauncher::onLoginComplete() { + // TODO: Fix this. + /* LoginTask *task = (LoginTask *)QObject::sender(); auto result = task->getResult(); auto instance = MMC->instances()->getInstanceById(instId); @@ -52,6 +54,7 @@ void InstanceLauncher::onLoginComplete() proc->setLogin(result.username, result.session_id); proc->launch(); + */ } void InstanceLauncher::doLogin(const QString &errorMsg) diff --git a/logic/LegacyInstance.cpp b/logic/LegacyInstance.cpp index 07e302f5..ab6536d0 100644 --- a/logic/LegacyInstance.cpp +++ b/logic/LegacyInstance.cpp @@ -50,7 +50,7 @@ Task *LegacyInstance::doUpdate() return new LegacyUpdate(this, this); } -MinecraftProcess *LegacyInstance::prepareForLaunch(LoginResponse response) +MinecraftProcess *LegacyInstance::prepareForLaunch(MojangAccountPtr account) { MinecraftProcess *proc = new MinecraftProcess(this); @@ -103,8 +103,8 @@ MinecraftProcess *LegacyInstance::prepareForLaunch(LoginResponse response) #endif args << "-jar" << LAUNCHER_FILE; - args << response.player_name; - args << response.session_id; + args << account->currentProfile()->name(); + args << account->sessionId(); args << windowTitle; args << windowSize; args << lwjgl; diff --git a/logic/LegacyInstance.h b/logic/LegacyInstance.h index 04b16b9b..3d35521e 100644 --- a/logic/LegacyInstance.h +++ b/logic/LegacyInstance.h @@ -80,7 +80,7 @@ public: virtual void setShouldUpdate(bool val); virtual Task *doUpdate(); - virtual MinecraftProcess *prepareForLaunch(LoginResponse response); + virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account); virtual void cleanupAfterRun(); virtual QDialog *createModEditDialog(QWidget *parent); @@ -93,4 +93,4 @@ public: protected slots: virtual void jarModsChanged(); -}; \ No newline at end of file +}; diff --git a/logic/OneSixInstance.cpp b/logic/OneSixInstance.cpp index 7764d225..a947b7c0 100644 --- a/logic/OneSixInstance.cpp +++ b/logic/OneSixInstance.cpp @@ -66,7 +66,7 @@ QString replaceTokensIn(QString text, QMap with) return result; } -QStringList OneSixInstance::processMinecraftArgs(LoginResponse response) +QStringList OneSixInstance::processMinecraftArgs(MojangAccountPtr account) { I_D(OneSixInstance); auto version = d->version; @@ -74,11 +74,11 @@ QStringList OneSixInstance::processMinecraftArgs(LoginResponse response) QMap token_mapping; // yggdrasil! - token_mapping["auth_username"] = response.username; - token_mapping["auth_session"] = response.session_id; - token_mapping["auth_access_token"] = response.access_token; - token_mapping["auth_player_name"] = response.player_name; - token_mapping["auth_uuid"] = response.player_id; + token_mapping["auth_username"] = account->username(); + token_mapping["auth_session"] = account->sessionId(); + token_mapping["auth_access_token"] = account->accessToken(); + token_mapping["auth_player_name"] = account->currentProfile()->name(); + token_mapping["auth_uuid"] = account->currentProfile()->id(); // this is for offline?: /* @@ -105,7 +105,7 @@ QStringList OneSixInstance::processMinecraftArgs(LoginResponse response) return parts; } -MinecraftProcess *OneSixInstance::prepareForLaunch(LoginResponse response) +MinecraftProcess *OneSixInstance::prepareForLaunch(MojangAccountPtr account) { I_D(OneSixInstance); cleanupAfterRun(); @@ -177,7 +177,7 @@ MinecraftProcess *OneSixInstance::prepareForLaunch(LoginResponse response) args << classPath; } args << version->mainClass; - args.append(processMinecraftArgs(response)); + args.append(processMinecraftArgs(account)); // Set the width and height for 1.6 instances bool maximize = settings().get("LaunchMaximized").toBool(); diff --git a/logic/OneSixInstance.h b/logic/OneSixInstance.h index 2d44ddba..e30ca7ca 100644 --- a/logic/OneSixInstance.h +++ b/logic/OneSixInstance.h @@ -40,7 +40,8 @@ public: virtual QString instanceConfigFolder() const; virtual Task *doUpdate(); - virtual MinecraftProcess *prepareForLaunch(LoginResponse response); + virtual MinecraftProcess *prepareForLaunch(MojangAccountPtr account); + virtual void cleanupAfterRun(); virtual QString intendedVersionId() const; @@ -72,5 +73,5 @@ public: virtual QString getStatusbarDescription(); private: - QStringList processMinecraftArgs(LoginResponse response); -}; \ No newline at end of file + QStringList processMinecraftArgs(MojangAccountPtr account); +}; diff --git a/logic/auth/AuthenticateTask.cpp b/logic/auth/AuthenticateTask.cpp new file mode 100644 index 00000000..a9c2c03f --- /dev/null +++ b/logic/auth/AuthenticateTask.cpp @@ -0,0 +1,180 @@ + +/* Copyright 2013 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 + +#include + +#include +#include +#include +#include +#include + +#include "logger/QsLog.h" + +AuthenticateTask::AuthenticateTask(MojangAccountPtr account, const QString& password, QObject* parent) : + YggdrasilTask(account, parent), m_password(password) +{ +} + +QJsonObject AuthenticateTask::getRequestContent() const +{ + /* + * { + * "agent": { // optional + * "name": "Minecraft", // So far this is the only encountered value + * "version": 1 // This number might be increased + * // by the vanilla client in the future + * }, + * "username": "mojang account name", // Can be an email address or player name for + // unmigrated accounts + * "password": "mojang account password", + * "clientToken": "client identifier" // optional + * } + */ + QJsonObject req; + + { + QJsonObject agent; + // C++ makes string literals void* for some stupid reason, so we have to tell it QString... Thanks Obama. + agent.insert("name", QString("Minecraft")); + agent.insert("version", 1); + req.insert("agent", agent); + } + + req.insert("username", getMojangAccount()->username()); + req.insert("password", m_password); + + // If we already have a client token, give it to the server. + // Otherwise, let the server give us one. + if (!getMojangAccount()->clientToken().isEmpty()) + req.insert("clientToken", getMojangAccount()->clientToken()); + + return req; +} + +bool AuthenticateTask::processResponse(QJsonObject responseData) +{ + // Read the response data. We need to get the client token, access token, and the selected profile. + QLOG_DEBUG() << "Processing authentication response."; + + // If we already have a client token, make sure the one the server gave us matches our existing one. + QLOG_DEBUG() << "Getting client token."; + QString clientToken = responseData.value("clientToken").toString(""); + if (clientToken.isEmpty()) + { + // Fail if the server gave us an empty client token + // TODO: Set an error properly to display to the user. + QLOG_ERROR() << "Server didn't send a client token."; + return false; + } + if (!getMojangAccount()->clientToken().isEmpty() && clientToken != getMojangAccount()->clientToken()) + { + // The server changed our client token! Obey its wishes, but complain. That's what I do for my parents, so... + QLOG_WARN() << "Server changed our client token to '" << clientToken + << "'. This shouldn't happen, but it isn't really a big deal."; + } + // Set the client token. + getMojangAccount()->setClientToken(clientToken); + + + // Now, we set the access token. + QLOG_DEBUG() << "Getting access token."; + QString accessToken = responseData.value("accessToken").toString(""); + if (accessToken.isEmpty()) + { + // Fail if the server didn't give us an access token. + // TODO: Set an error properly to display to the user. + QLOG_ERROR() << "Server didn't send an access token."; + } + // Set the access token. + getMojangAccount()->setAccessToken(accessToken); + + + // Now we load the list of available profiles. + // Mojang hasn't yet implemented the profile system, + // but we might as well support what's there so we + // don't have trouble implementing it later. + QLOG_DEBUG() << "Loading profile list."; + QJsonArray availableProfiles = responseData.value("availableProfiles").toArray(); + ProfileList loadedProfiles; + for (auto iter : availableProfiles) + { + QJsonObject profile = iter.toObject(); + // Profiles are easy, we just need their ID and name. + QString id = profile.value("id").toString(""); + QString name = profile.value("name").toString(""); + + if (id.isEmpty() || name.isEmpty()) + { + // This should never happen, but we might as well + // warn about it if it does so we can debug it easily. + // You never know when Mojang might do something truly derpy. + QLOG_WARN() << "Found entry in available profiles list with missing ID or name field. Ignoring it."; + } + + // Now, add a new AccountProfile entry to the list. + loadedProfiles.append(AccountProfile(id, name)); + } + // Put the list of profiles we loaded into the MojangAccount object. + getMojangAccount()->loadProfiles(loadedProfiles); + + + // Finally, we set the current profile to the correct value. This is pretty simple. + // We do need to make sure that the current profile that the server gave us + // is actually in the available profiles list. + // If it isn't, we'll just fail horribly (*shouldn't* ever happen, but you never know). + QLOG_DEBUG() << "Setting current profile."; + QJsonObject currentProfile = responseData.value("selectedProfile").toObject(); + QString currentProfileId = currentProfile.value("id").toString(""); + if (currentProfileId.isEmpty()) + { + // TODO: Set an error to display to the user. + QLOG_ERROR() << "Server didn't specify a currently selected profile."; + return false; + } + if (!getMojangAccount()->setProfile(currentProfileId)) + { + // TODO: Set an error to display to the user. + QLOG_ERROR() << "Server specified a selected profile that wasn't in the available profiles list."; + return false; + } + + + // We've made it through the minefield of possible errors. Return true to indicate that we've succeeded. + QLOG_DEBUG() << "Finished reading authentication response."; + return true; +} + +QString AuthenticateTask::getEndpoint() const +{ + return "authenticate"; +} + +QString AuthenticateTask::getStateMessage(const YggdrasilTask::State state) const +{ + switch (state) + { + case STATE_SENDING_REQUEST: + return tr("Authenticating: Sending request."); + case STATE_PROCESSING_RESPONSE: + return tr("Authenticating: Processing response."); + default: + return YggdrasilTask::getStateMessage(state); + } +} + diff --git a/logic/auth/AuthenticateTask.h b/logic/auth/AuthenticateTask.h new file mode 100644 index 00000000..54a6b79a --- /dev/null +++ b/logic/auth/AuthenticateTask.h @@ -0,0 +1,46 @@ +/* Copyright 2013 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 +#include + +/** + * The authenticate task takes a MojangAccount with no access token and password and attempts to authenticate with Mojang's servers. + * If successful, it will set the MojangAccount's access token. + */ +class AuthenticateTask : public YggdrasilTask +{ +Q_OBJECT +public: + AuthenticateTask(MojangAccountPtr account, const QString& password, QObject* parent=0); + +protected: + virtual QJsonObject getRequestContent() const; + + virtual QString getEndpoint() const; + + virtual bool processResponse(QJsonObject responseData); + + QString getStateMessage(const YggdrasilTask::State state) const; + +private: + QString m_password; +}; + diff --git a/logic/auth/MojangAccount.cpp b/logic/auth/MojangAccount.cpp new file mode 100644 index 00000000..4f3839bc --- /dev/null +++ b/logic/auth/MojangAccount.cpp @@ -0,0 +1,220 @@ +/* Copyright 2013 MultiMC Contributors + * + * Authors: Orochimarufan + * + * 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 "MojangAccount.h" + +#include +#include +#include + +#include + +MojangAccount::MojangAccount(const QString& username, QObject* parent) : + QObject(parent) +{ + // Generate a client token. + m_clientToken = QUuid::createUuid().toString(); + + m_username = username; + + m_currentProfile = -1; +} + +MojangAccount::MojangAccount(const QString& username, const QString& clientToken, + const QString& accessToken, QObject* parent) : + QObject(parent) +{ + m_username = username; + m_clientToken = clientToken; + m_accessToken = accessToken; + + m_currentProfile = -1; +} + +MojangAccount::MojangAccount(const MojangAccount& other, QObject* parent) +{ + m_username = other.username(); + m_clientToken = other.clientToken(); + m_accessToken = other.accessToken(); + + m_profiles = other.m_profiles; + m_currentProfile = other.m_currentProfile; +} + + +QString MojangAccount::username() const +{ + return m_username; +} + +QString MojangAccount::clientToken() const +{ + return m_clientToken; +} + +void MojangAccount::setClientToken(const QString& clientToken) +{ + m_clientToken = clientToken; +} + + +QString MojangAccount::accessToken() const +{ + return m_accessToken; +} + +void MojangAccount::setAccessToken(const QString& accessToken) +{ + m_accessToken = accessToken; +} + +QString MojangAccount::sessionId() const +{ + return "token:" + m_accessToken + ":" + currentProfile()->id(); +} + +const QList MojangAccount::profiles() const +{ + return m_profiles; +} + +const AccountProfile* MojangAccount::currentProfile() const +{ + if (m_currentProfile < 0) + { + if (m_profiles.length() > 0) + return &m_profiles.at(0); + else + return nullptr; + } + else + return &m_profiles.at(m_currentProfile); +} + +bool MojangAccount::setProfile(const QString& profileId) +{ + const QList& profiles = this->profiles(); + for (int i = 0; i < profiles.length(); i++) + { + if (profiles.at(i).id() == profileId) + { + m_currentProfile = i; + return true; + } + } + return false; +} + +void MojangAccount::loadProfiles(const ProfileList& profiles) +{ + m_profiles.clear(); + for (auto profile : profiles) + m_profiles.append(profile); +} + +MojangAccountPtr MojangAccount::loadFromJson(const QJsonObject& object) +{ + // The JSON object must at least have a username for it to be valid. + if (!object.value("username").isString()) + { + QLOG_ERROR() << "Can't load Mojang account info from JSON object. Username field is missing or of the wrong type."; + return nullptr; + } + + QString username = object.value("username").toString(""); + QString clientToken = object.value("clientToken").toString(""); + QString accessToken = object.value("accessToken").toString(""); + + QJsonArray profileArray = object.value("profiles").toArray(); + if (profileArray.size() < 1) + { + QLOG_ERROR() << "Can't load Mojang account with username \"" << username << "\". No profiles found."; + return nullptr; + } + + ProfileList profiles; + for (QJsonValue profileVal : profileArray) + { + QJsonObject profileObject = profileVal.toObject(); + QString id = profileObject.value("id").toString(""); + QString name = profileObject.value("name").toString(""); + if (id.isEmpty() || name.isEmpty()) + { + QLOG_WARN() << "Unable to load a profile because it was missing an ID or a name."; + continue; + } + profiles.append(AccountProfile(id, name)); + } + + MojangAccountPtr account(new MojangAccount(username, clientToken, accessToken)); + account->loadProfiles(profiles); + + // Get the currently selected profile. + QString currentProfile = object.value("activeProfile").toString(""); + if (!currentProfile.isEmpty()) + account->setProfile(currentProfile); + + return account; +} + +QJsonObject MojangAccount::saveToJson() +{ + QJsonObject json; + json.insert("username", username()); + json.insert("clientToken", clientToken()); + json.insert("accessToken", accessToken()); + + QJsonArray profileArray; + for (AccountProfile profile : m_profiles) + { + QJsonObject profileObj; + profileObj.insert("id", profile.id()); + profileObj.insert("name", profile.name()); + profileArray.append(profileObj); + } + json.insert("profiles", profileArray); + + if (currentProfile() != nullptr) + json.insert("activeProfile", currentProfile()->id()); + + return json; +} + + +AccountProfile::AccountProfile(const QString& id, const QString& name) +{ + m_id = id; + m_name = name; +} + +AccountProfile::AccountProfile(const AccountProfile& other) +{ + m_id = other.m_id; + m_name = other.m_name; +} + +QString AccountProfile::id() const +{ + return m_id; +} + +QString AccountProfile::name() const +{ + return m_name; +} + + diff --git a/logic/auth/MojangAccount.h b/logic/auth/MojangAccount.h new file mode 100644 index 00000000..062b8aa2 --- /dev/null +++ b/logic/auth/MojangAccount.h @@ -0,0 +1,154 @@ +/* Copyright 2013 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 +#include + +#include + +class MojangAccount; + +typedef std::shared_ptr MojangAccountPtr; +Q_DECLARE_METATYPE(MojangAccountPtr) + + +/** + * Class that represents a profile within someone's Mojang account. + * + * Currently, the profile system has not been implemented by Mojang yet, + * but we might as well add some things for it in MultiMC right now so + * we don't have to rip the code to pieces to add it later. + */ +class AccountProfile +{ +public: + AccountProfile(const QString& id, const QString& name); + AccountProfile(const AccountProfile& other); + + QString id() const; + QString name() const; +protected: + QString m_id; + QString m_name; +}; + + +typedef QList ProfileList; + + +/** + * Object that stores information about a certain Mojang account. + * + * Said information may include things such as that account's username, client token, and access + * token if the user chose to stay logged in. + */ +class MojangAccount : public QObject +{ +Q_OBJECT +public: + /** + * Constructs a new MojangAccount with the given username. + * The client token will be generated automatically and the access token will be blank. + */ + explicit MojangAccount(const QString& username, QObject* parent = 0); + + /** + * Constructs a new MojangAccount with the given username, client token, and access token. + */ + explicit MojangAccount(const QString& username, const QString& clientToken, const QString& accessToken, QObject* parent = 0); + + /** + * Constructs a new MojangAccount matching the given account. + */ + MojangAccount(const MojangAccount& other, QObject* parent); + + /** + * Loads a MojangAccount from the given JSON object. + */ + static MojangAccountPtr loadFromJson(const QJsonObject& json); + + /** + * Saves a MojangAccount to a JSON object and returns it. + */ + QJsonObject saveToJson(); + + + /** + * This MojangAccount's username. May be an email address if the account is migrated. + */ + QString username() const; + + /** + * This MojangAccount's client token. This is a UUID used by Mojang's auth servers to identify this client. + * This is unique for each MojangAccount. + */ + QString clientToken() const; + + /** + * Sets the MojangAccount's client token to the given value. + */ + void setClientToken(const QString& token); + + /** + * This MojangAccount's access token. + * If the user has not chosen to stay logged in, this will be an empty string. + */ + QString accessToken() const; + + /** + * Changes this MojangAccount's access token to the given value. + */ + void setAccessToken(const QString& token); + + /** + * Get full session ID + */ + QString sessionId() const; + + /** + * Returns a list of the available account profiles. + */ + const ProfileList profiles() const; + + /** + * Returns a pointer to the currently selected profile. + * If no profile is selected, returns the first profile in the profile list or nullptr if there are none. + */ + const AccountProfile* currentProfile() const; + + /** + * Sets the currently selected profile to the profile with the given ID string. + * If profileId is not in the list of available profiles, the function will simply return false. + */ + bool setProfile(const QString& profileId); + + /** + * Clears the current account profile list and replaces it with the given profile list. + */ + void loadProfiles(const ProfileList& profiles); + + +protected: + QString m_username; + QString m_clientToken; + QString m_accessToken; // Blank if not logged in. + int m_currentProfile; // Index of the selected profile within the list of available profiles. -1 if nothing is selected. + ProfileList m_profiles; // List of available profiles. +}; + diff --git a/logic/auth/YggdrasilTask.cpp b/logic/auth/YggdrasilTask.cpp new file mode 100644 index 00000000..39dfb749 --- /dev/null +++ b/logic/auth/YggdrasilTask.cpp @@ -0,0 +1,182 @@ +/* Copyright 2013 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 + +#include +#include +#include +#include +#include +#include + +#include +#include + +YggdrasilTask::YggdrasilTask(MojangAccountPtr account, QObject* parent) : Task(parent) +{ + m_error = nullptr; + m_account = account; +} + + +YggdrasilTask::~YggdrasilTask() +{ + if (m_error) + delete m_error; +} + +void YggdrasilTask::executeTask() +{ + setStatus(getStateMessage(STATE_SENDING_REQUEST)); + + // Get the content of the request we're going to send to the server. + QJsonDocument doc(getRequestContent()); + + auto worker = MMC->qnam(); + connect(worker.get(), SIGNAL(finished(QNetworkReply*)), this, + SLOT(processReply(QNetworkReply*))); + + QUrl reqUrl("https://authserver.mojang.com/" + getEndpoint()); + QNetworkRequest netRequest(reqUrl); + netRequest.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); + + m_netReply = worker->post(netRequest, doc.toJson()); +} + +void YggdrasilTask::processReply(QNetworkReply* reply) +{ + setStatus(getStateMessage(STATE_PROCESSING_RESPONSE)); + + if (m_netReply != reply) + // Wrong reply for some reason... + return; + + // Check for errors. + switch (reply->error()) + { + case QNetworkReply::NoError: + { + // Try to parse the response regardless of the response code. + // Sometimes the auth server will give more information and an error code. + QJsonParseError jsonError; + QByteArray replyData = reply->readAll(); + QJsonDocument doc = QJsonDocument::fromJson(replyData, &jsonError); + + // Check the response code. + int responseCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + + switch (responseCode) + { + case 200: + { + // If the response code was 200, then there shouldn't be an error. Make sure anyways. + switch (jsonError.error) + { + case QJsonParseError::NoError: + if (!processResponse(doc.object())) + { + YggdrasilTask::Error* err = getError(); + if (err) + emitFailed(err->getErrorMessage()); + else + emitFailed(tr("An unknown error occurred when processing the response from the authentication server.")); + } + else + { + emitSucceeded(); + } + break; + + default: + emitFailed(tr("Failed to parse Yggdrasil JSON response: \"%1\".").arg(jsonError.errorString())); + break; + } + break; + } + + default: + // If the response code was something else, then Yggdrasil may have given us information about the error. + // If we can parse the response, then get information from it. Otherwise just say there was an unknown error. + switch (jsonError.error) + { + case QJsonParseError::NoError: + // We were able to parse the server's response. Woo! + // Call processError. If a subclass has overridden it then they'll handle their stuff there. + processError(doc.object()); + break; + + default: + // The server didn't say anything regarding the error. Give the user an unknown error. + emitFailed(tr("Login failed: Unknown HTTP code %1 encountered.").arg(responseCode)); + break; + } + break; + } + + break; + } + + case QNetworkReply::OperationCanceledError: + emitFailed(tr("Login canceled.")); + break; + + default: + emitFailed(tr("An unknown error occurred when trying to communicate with the authentication server.")); + break; + } +} + +QString YggdrasilTask::processError(QJsonObject responseData) +{ + QJsonValue errorVal = responseData.value("error"); + QJsonValue msgVal = responseData.value("errorMessage"); + QJsonValue causeVal = responseData.value("cause"); + + if (errorVal.isString() && msgVal.isString() && causeVal.isString()) + { + m_error = new Error(errorVal.toString(""), msgVal.toString(""), causeVal.toString("")); + return m_error->getDisplayMessage(); + } + else + { + // Error is not in standard format. Don't set m_error and return unknown error. + return tr("An unknown Yggdrasil error occurred."); + } +} + +QString YggdrasilTask::getStateMessage(const YggdrasilTask::State state) const +{ + switch (state) + { + case STATE_SENDING_REQUEST: + return tr("Sending request to auth servers."); + case STATE_PROCESSING_RESPONSE: + return tr("Processing response from servers."); + default: + return tr("Processing. Please wait."); + } +} + +YggdrasilTask::Error *YggdrasilTask::getError() const +{ + return this->m_error; +} + +MojangAccountPtr YggdrasilTask::getMojangAccount() const +{ + return this->m_account; +} + diff --git a/logic/auth/YggdrasilTask.h b/logic/auth/YggdrasilTask.h new file mode 100644 index 00000000..6aebae16 --- /dev/null +++ b/logic/auth/YggdrasilTask.h @@ -0,0 +1,128 @@ +/* Copyright 2013 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 + +#include "logic/auth/MojangAccount.h" + +class QNetworkReply; + + +/** + * A Yggdrasil task is a task that performs an operation on a given mojang account. + */ +class YggdrasilTask : public Task +{ +Q_OBJECT +public: + explicit YggdrasilTask(MojangAccountPtr account, QObject* parent=0); + ~YggdrasilTask(); + + /** + * Class describing a Yggdrasil error response. + */ + class Error + { + public: + Error(const QString& shortError, const QString& errorMessage, const QString& cause) : + m_shortError(shortError), m_errorMessage(errorMessage), m_cause(cause) {} + + QString getShortError() const { return m_shortError; } + QString getErrorMessage() const { return m_errorMessage; } + QString getCause() const { return m_cause; } + + /// Gets the string to display in the GUI for describing this error. + QString getDisplayMessage() { return getErrorMessage(); } + + protected: + QString m_shortError; + QString m_errorMessage; + QString m_cause; + }; + + /** + * Gets the Mojang account that this task is operating on. + */ + virtual MojangAccountPtr getMojangAccount() const; + + /** + * Returns a pointer to a YggdrasilTask::Error object if an error has occurred. + * If no error has occurred, returns a null pointer. + */ + virtual Error* getError() const; + +protected: + /** + * Enum for describing the state of the current task. + * Used by the getStateMessage function to determine what the status message should be. + */ + enum State + { + STATE_SENDING_REQUEST, + STATE_PROCESSING_RESPONSE, + STATE_OTHER, + }; + + virtual void executeTask(); + + /** + * Gets the JSON object that will be sent to the authentication server. + * Should be overridden by subclasses. + */ + virtual QJsonObject getRequestContent() const = 0; + + /** + * Gets the endpoint to POST to. + * No leading slash. + */ + virtual QString getEndpoint() const = 0; + + /** + * Processes the response received from the server. + * If an error occurred, this should emit a failed signal and return false. + * If Yggdrasil gave an error response, it should call setError() first, and then return false. + * Otherwise, it should return true. + */ + virtual bool processResponse(QJsonObject responseData) = 0; + + /** + * Processes an error response received from the server. + * The default implementation will read data from Yggdrasil's standard error response format and set it as this task's Error. + * \returns a QString error message that will be passed to emitFailed. + */ + virtual QString processError(QJsonObject responseData); + + /** + * Returns the state message for the given state. + * Used to set the status message for the task. + * Should be overridden by subclasses that want to change messages for a given state. + */ + virtual QString getStateMessage(const State state) const; + + MojangAccountPtr m_account; + + QNetworkReply* m_netReply; + + Error* m_error; + +protected slots: + void processReply(QNetworkReply* reply); +}; + diff --git a/logic/lists/MojangAccountList.cpp b/logic/lists/MojangAccountList.cpp new file mode 100644 index 00000000..1d67c70f --- /dev/null +++ b/logic/lists/MojangAccountList.cpp @@ -0,0 +1,349 @@ +/* Copyright 2013 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 "logic/lists/MojangAccountList.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "logger/QsLog.h" + +#include "logic/auth/MojangAccount.h" + +#define ACCOUNT_LIST_FORMAT_VERSION 1 + +MojangAccountList::MojangAccountList(QObject *parent) : QAbstractListModel(parent) +{ +} + +MojangAccountPtr MojangAccountList::findAccount(const QString &username) const +{ + for (int i = 0; i < count(); i++) + { + MojangAccountPtr account = at(i); + if (account->username() == username) + return account; + } + return nullptr; +} + + +const MojangAccountPtr MojangAccountList::at(int i) const +{ + return MojangAccountPtr(m_accounts.at(i)); +} + +void MojangAccountList::addAccount(const MojangAccountPtr account) +{ + beginResetModel(); + m_accounts.append(account); + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(const QString& username) +{ + beginResetModel(); + for (auto account : m_accounts) + { + if (account->username() == username) + { + m_accounts.removeOne(account); + return; + } + } + endResetModel(); + onListChanged(); +} + +void MojangAccountList::removeAccount(QModelIndex index) +{ + beginResetModel(); + m_accounts.removeAt(index.row()); + endResetModel(); + onListChanged(); +} + + +MojangAccountPtr MojangAccountList::activeAccount() const +{ + if (m_activeAccount.isEmpty()) + return nullptr; + else + return findAccount(m_activeAccount); +} + +void MojangAccountList::setActiveAccount(const QString& username) +{ + beginResetModel(); + for (MojangAccountPtr account : m_accounts) + if (account->username() == username) + m_activeAccount = username; + endResetModel(); + onListChanged(); +} + + +void MojangAccountList::onListChanged() +{ + if (m_autosave) + // TODO: Alert the user if this fails. + saveList(); + emit listChanged(); +} + + +int MojangAccountList::count() const +{ + return m_accounts.count(); +} + + +QVariant MojangAccountList::data(const QModelIndex &index, int role) const +{ + if (!index.isValid()) + return QVariant(); + + if (index.row() > count()) + return QVariant(); + + MojangAccountPtr account = at(index.row()); + + switch (role) + { + case Qt::DisplayRole: + switch (index.column()) + { + case ActiveColumn: + return account->username() == m_activeAccount; + + case NameColumn: + return account->username(); + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + return account->username(); + + case PointerRole: + return qVariantFromValue(account); + + default: + return QVariant(); + } +} + +QVariant MojangAccountList::headerData(int section, Qt::Orientation orientation, int role) const +{ + switch (role) + { + case Qt::DisplayRole: + switch (section) + { + case ActiveColumn: + return "Active?"; + + case NameColumn: + return "Name"; + + default: + return QVariant(); + } + + case Qt::ToolTipRole: + switch (section) + { + case NameColumn: + return "The name of the version."; + + default: + return QVariant(); + } + + default: + return QVariant(); + } +} + +int MojangAccountList::rowCount(const QModelIndex &parent) const +{ + // Return count + return count(); +} + +int MojangAccountList::columnCount(const QModelIndex &parent) const +{ + return 2; +} + +void MojangAccountList::updateListData(QList versions) +{ + beginResetModel(); + m_accounts = versions; + endResetModel(); +} + +bool MojangAccountList::loadList(const QString& filePath) +{ + QString path = filePath; + if (path.isEmpty()) path = m_listFilePath; + if (path.isEmpty()) + { + QLOG_ERROR() << "Can't load Mojang account list. No file path given and no default set."; + return false; + } + + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::ReadOnly)) + { + QLOG_ERROR() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Read the file and close it. + QByteArray jsonData = file.readAll(); + file.close(); + + QJsonParseError parseError; + QJsonDocument jsonDoc = QJsonDocument::fromJson(jsonData, &parseError); + + // Fail if the JSON is invalid. + if (parseError.error != QJsonParseError::NoError) + { + QLOG_ERROR() << QString("Failed to parse account list file: %1 at offset %2") + .arg(parseError.errorString(), QString::number(parseError.offset)) + .toUtf8(); + return false; + } + + // Make sure the root is an object. + if (!jsonDoc.isObject()) + { + QLOG_ERROR() << "Invalid account list JSON: Root should be an array."; + return false; + } + + QJsonObject root = jsonDoc.object(); + + // Make sure the format version matches. + if (root.value("formatVersion").toVariant().toInt() != ACCOUNT_LIST_FORMAT_VERSION) + { + QString newName = "accounts-old.json"; + QLOG_WARN() << "Format version mismatch when loading account list. Existing one will be renamed to" + << newName; + + // Attempt to rename the old version. + file.rename(newName); + return false; + } + + // Now, load the accounts array. + beginResetModel(); + QJsonArray accounts = root.value("accounts").toArray(); + for (QJsonValue accountVal : accounts) + { + QJsonObject accountObj = accountVal.toObject(); + MojangAccountPtr account = MojangAccount::loadFromJson(accountObj); + if (account.get() != nullptr) + { + m_accounts.append(account); + } + else + { + QLOG_WARN() << "Failed to load an account."; + } + } + endResetModel(); + + // Load the active account. + m_activeAccount = root.value("activeAccount").toString(""); + + return true; +} + +bool MojangAccountList::saveList(const QString& filePath) +{ + QString path(filePath); + if (path.isEmpty()) path = m_listFilePath; + if (path.isEmpty()) + { + QLOG_ERROR() << "Can't save Mojang account list. No file path given and no default set."; + return false; + } + + QLOG_INFO() << "Writing account list to" << path; + + QLOG_DEBUG() << "Building JSON data structure."; + // Build the JSON document to write to the list file. + QJsonObject root; + + root.insert("formatVersion", ACCOUNT_LIST_FORMAT_VERSION); + + // Build a list of accounts. + QLOG_DEBUG() << "Building account array."; + QJsonArray accounts; + for (MojangAccountPtr account : m_accounts) + { + QJsonObject accountObj = account->saveToJson(); + accounts.append(accountObj); + } + + // Insert the account list into the root object. + root.insert("accounts", accounts); + + // Save the active account. + root.insert("activeAccount", m_activeAccount); + + // Create a JSON document object to convert our JSON to bytes. + QJsonDocument doc(root); + + + // Now that we're done building the JSON object, we can write it to the file. + QLOG_DEBUG() << "Writing account list to file."; + QFile file(path); + + // Try to open the file and fail if we can't. + // TODO: We should probably report this error to the user. + if (!file.open(QIODevice::WriteOnly)) + { + QLOG_ERROR() << QString("Failed to read the account list file (%1).").arg(path).toUtf8(); + return false; + } + + // Write the JSON to the file. + file.write(doc.toJson()); + file.close(); + + QLOG_INFO() << "Saved account list to" << path; + + return true; +} + +void MojangAccountList::setListFilePath(QString path, bool autosave) +{ + m_listFilePath = path; + autosave = autosave; +} + diff --git a/logic/lists/MojangAccountList.h b/logic/lists/MojangAccountList.h new file mode 100644 index 00000000..71f472f7 --- /dev/null +++ b/logic/lists/MojangAccountList.h @@ -0,0 +1,175 @@ +/* Copyright 2013 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 +#include + +#include "logic/auth/MojangAccount.h" + + +/*! + * \brief List of available Mojang accounts. + * This should be loaded in the background by MultiMC on startup. + * + * This class also inherits from QAbstractListModel. Methods from that + * class determine how this list shows up in a list view. Said methods + * all have a default implementation, but they can be overridden by subclasses to + * change the behavior of the list. + */ +class MojangAccountList : public QAbstractListModel +{ + Q_OBJECT +public: + enum ModelRoles + { + PointerRole = 0x34B1CB48 + }; + + enum VListColumns + { + // TODO: Add icon column. + + // First column - Active? + ActiveColumn = 0, + + // Second column - Name + NameColumn, + }; + + explicit MojangAccountList(QObject *parent = 0); + + //! Gets the account at the given index. + virtual const MojangAccountPtr at(int i) const; + + //! Returns the number of accounts in the list. + virtual int count() const; + + //////// List Model Functions //////// + virtual QVariant data(const QModelIndex &index, int role) const; + virtual QVariant headerData(int section, Qt::Orientation orientation, int role) const; + virtual int rowCount(const QModelIndex &parent) const; + virtual int columnCount(const QModelIndex &parent) const; + + /*! + * Adds a the given Mojang account to the account list. + */ + virtual void addAccount(const MojangAccountPtr account); + + /*! + * Removes the mojang account with the given username from the account list. + */ + virtual void removeAccount(const QString& username); + + /*! + * Removes the account at the given QModelIndex. + */ + virtual void removeAccount(QModelIndex index); + + /*! + * \brief Finds an account by its username. + * \param The username of the account to find. + * \return A const pointer to the account with the given username. NULL if + * one doesn't exist. + */ + virtual MojangAccountPtr findAccount(const QString &username) const; + + /*! + * Sets the default path to save the list file to. + * If autosave is true, this list will automatically save to the given path whenever it changes. + * THIS FUNCTION DOES NOT LOAD THE LIST. If you set autosave, be sure to call loadList() immediately + * after calling this function to ensure an autosaved change doesn't overwrite the list you intended + * to load. + */ + virtual void setListFilePath(QString path, bool autosave=false); + + /*! + * \brief Loads the account list from the given file path. + * If the given file is an empty string (default), will load from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool loadList(const QString& file=""); + + /*! + * \brief Saves the account list to the given file. + * If the given file is an empty string (default), will save from the default account list file. + * \return True if successful, otherwise false. + */ + virtual bool saveList(const QString& file=""); + + /*! + * \brief Gets a pointer to the account that the user has selected as their "active" account. + * Which account is active can be overridden on a per-instance basis, but this will return the one that + * is set as active globally. + * \return The currently active MojangAccount. If there isn't an active account, returns a null pointer. + */ + virtual MojangAccountPtr activeAccount() const; + + /*! + * Sets the given account as the current active account. + */ + virtual void setActiveAccount(const QString& username); + +signals: + /*! + * Signal emitted to indicate that the account list has changed. + * This will also fire if the value of an element in the list changes (will be implemented later). + */ + void listChanged(); + +protected: + /*! + * Called whenever the list changes. + * This emits the listChanged() signal and autosaves the list (if autosave is enabled). + */ + void onListChanged(); + + QList m_accounts; + + /*! + * Username of the account that is currently active. + * Empty string if no account is active. + */ + QString m_activeAccount; + + //! Path to the account list file. Empty string if there isn't one. + QString m_listFilePath; + + /*! + * If true, the account list will automatically save to the account list path when it changes. + * Ignored if m_listFilePath is blank. + */ + bool m_autosave; + +protected +slots: + /*! + * Updates this list with the given list of accounts. + * This is done by copying each account in the given list and inserting it + * into this one. + * We need to do this so that we can set the parents of the accounts are set to this + * account list. This can't be done in the load task, because the accounts the load + * task creates are on the load task's thread and Qt won't allow their parents + * to be set to something created on another thread. + * To get around that problem, we invoke this method on the GUI thread, which + * then copies the accounts and sets their parents correctly. + * \param accounts List of accounts whose parents should be set. + */ + virtual void updateListData(QList versions); +}; +