diff --git a/CMakeLists.txt b/CMakeLists.txt index 03bb5d3e..4ced1bcd 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -117,6 +117,9 @@ set(Launcher_QT_VERSION_MAJOR "5" CACHE STRING "Major Qt version to build agains # MSA Client ID execute_process(COMMAND ./id.sh OUTPUT_VARIABLE Launcher_MSA_CLIENT_ID) +set(Launcher_CURSEFORGE_API_KEY "" CACHE STRING "API key for the CurseForge platform") +set(Launcher_CURSEFORGE_API_KEY_API_URL "https://cf.polymc.org/api" CACHE STRING "URL to fetch the Curseforge API key from.") + # Curseforge API Key execute_process(COMMAND ./cf.sh OUTPUT_VARIABLE Launcher_CURSEFORGE_API_KEY) diff --git a/README.md b/README.md index e959f278..292e57d3 100644 --- a/README.md +++ b/README.md @@ -140,14 +140,8 @@ For people who don't want to use Discord, we have a Matrix Space which is bridge If there are any issues with the space or you are using a client that does not support the feature here are the individual rooms: -[![Development](https://img.shields.io/matrix/polymc-development:matrix.org?label=PolyMC%20Development)](https://matrix.to/#/#polymc-development:matrix.org) -[![Discussion](https://img.shields.io/matrix/polymc-discussion:matrix.org?label=PolyMC%20Discussion)](https://matrix.to/#/#polymc-discussion:matrix.org) -[![Github](https://img.shields.io/matrix/polymc-github:matrix.org?label=PolyMC%20Github)](https://matrix.to/#/#polymc-github:matrix.org) -[![Maintainers](https://img.shields.io/matrix/polymc-maintainers:matrix.org?label=PolyMC%20Maintainers)](https://matrix.to/#/#polymc-maintainers:matrix.org) [![News](https://img.shields.io/matrix/polymc-news:matrix.org?label=PolyMC%20News)](https://matrix.to/#/#polymc-news:matrix.org) -[![Offtopic](https://img.shields.io/matrix/polymc-offtopic:matrix.org?label=PolyMC%20Offtopic)](https://matrix.to/#/#polymc-offtopic:matrix.org) -[![Support](https://img.shields.io/matrix/polymc-support:matrix.org?label=PolyMC%20Support)](https://matrix.to/#/#polymc-support:matrix.org) -[![Voice](https://img.shields.io/matrix/polymc-voice:matrix.org?label=PolyMC%20Voice)](https://matrix.to/#/#polymc-voice:matrix.org) +[![Discussion](https://img.shields.io/matrix/polymc-discussion:matrix.org?label=PolyMC%20Discussion)](https://matrix.to/#/#polymc-discussion:matrix.org) We also have a subreddit you can post your issues and suggestions on: diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 336c5d6d..e8b6967c 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -91,6 +92,7 @@ Config::Config() IMGUR_CLIENT_ID = "@Launcher_IMGUR_CLIENT_ID@"; MSA_CLIENT_ID = "@Launcher_MSA_CLIENT_ID@"; FLAME_API_KEY = "@Launcher_CURSEFORGE_API_KEY@"; + FLAME_API_KEY_API_URL = "@Launcher_CURSEFORGE_API_KEY_API_URL@"; META_URL = "@Launcher_META_URL@"; BUG_TRACKER_URL = "@Launcher_BUG_TRACKER_URL@"; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index cb9b367c..587e3937 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -3,6 +3,7 @@ * PolyMC - Minecraft Launcher * Copyright (c) 2022 Jamie Mansfield * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -110,6 +111,11 @@ class Config { */ QString FLAME_API_KEY; + /** + * URL to fetch the Client API key for CurseForge from + */ + QString FLAME_API_KEY_API_URL; + /** * Metadata repository URL prefix */ diff --git a/launcher/Application.cpp b/launcher/Application.cpp index eb5ca0a1..87239577 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -97,6 +97,8 @@ #include "icons/IconList.h" #include "net/HttpMetaCache.h" +#include "ui/GuiUtil.h" + #include "java/JavaUtils.h" #include "tools/JProfiler.h" @@ -631,6 +633,7 @@ Application::Application(int &argc, char **argv) : QApplication(argc, argv) m_settings->set("FlameKeyOverride", flameKey); m_settings->reset("CFKeyOverride"); } + m_settings->registerSetting("FlameKeyShouldBeFetchedOnStartup", true); m_settings->registerSetting("UserAgentOverride", ""); // Init page provider @@ -902,6 +905,7 @@ void Application::setupWizardFinished(int status) void Application::performMainStartupAction() { m_status = Application::Initialized; + if(!m_instanceIdToLaunch.isEmpty()) { auto inst = instances()->getInstanceById(m_instanceIdToLaunch); @@ -931,6 +935,32 @@ void Application::performMainStartupAction() return; } } + + { + bool shouldFetch = m_settings->get("FlameKeyShouldBeFetchedOnStartup").toBool(); + if (!BuildConfig.FLAME_API_KEY_API_URL.isEmpty() && shouldFetch && !(capabilities() & Capability::SupportsFlame)) + { + auto response = QMessageBox::question(nullptr, + tr("Curseforge Core API Key"), + tr("Should PolyMC try to fetch the Official Curseforge Launcher's API Key? " + "Using this key technically breaks Curseforge's Terms of Service, but this distribution of PolyMC " + "does not come with a Curseforge API key by default, so without this key or another valid API key, " + "which you can always change in the settings, you won't be able to download Curseforge modpacks."), + QMessageBox::Yes | QMessageBox::No); + + if (response == QMessageBox::Yes) + { + QString apiKey = GuiUtil::fetchFlameKey(); + if (!apiKey.isEmpty()) + { + m_settings->set("FlameKeyOverride", apiKey); + updateCapabilities(); + } + } + } + m_settings->set("FlameKeyShouldBeFetchedOnStartup", false); + } + if(!m_mainWindow) { // normal main window diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index b9eec2fd..fa941af9 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -103,6 +103,8 @@ set(NET_SOURCES net/ChecksumValidator.h net/Download.cpp net/Download.h + net/FetchFlameAPIKey.cpp + net/FetchFlameAPIKey.h net/FileSink.cpp net/FileSink.h net/HttpMetaCache.cpp diff --git a/launcher/InstanceImportTask.h b/launcher/InstanceImportTask.h index ef70c819..9ff2c511 100644 --- a/launcher/InstanceImportTask.h +++ b/launcher/InstanceImportTask.h @@ -85,7 +85,6 @@ private slots: private: /* data */ NetJob::Ptr m_filesNetJob; - shared_qobject_ptr m_modIdResolver; QUrl m_sourceUrl; QString m_archivePath; bool m_downloadRequired = false; diff --git a/launcher/modplatform/flame/FileResolvingTask.cpp b/launcher/modplatform/flame/FileResolvingTask.cpp index 058d2471..74bf126b 100644 --- a/launcher/modplatform/flame/FileResolvingTask.cpp +++ b/launcher/modplatform/flame/FileResolvingTask.cpp @@ -41,7 +41,17 @@ void Flame::FileResolvingTask::netJobFinished() // job to check modrinth for blocked projects auto job = new NetJob("Modrinth check", m_network); blockedProjects = QMap(); - auto doc = Json::requireDocument(*result); + QJsonDocument doc; + + try { + doc = Json::requireDocument(*result); + } + catch (const JSONValidationError &e) { + qDebug() << "Flame::FileResolvingTask: Json Validation error: " << e.what(); + emitFailed(e.what()); + return; + } + auto array = Json::requireArray(doc.object()["data"]); for (QJsonValueRef file : array) { auto fileid = Json::requireInteger(Json::requireObject(file)["id"]); diff --git a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp index 48ac02e0..98e3a42c 100644 --- a/launcher/modplatform/flame/FlameInstanceCreationTask.cpp +++ b/launcher/modplatform/flame/FlameInstanceCreationTask.cpp @@ -335,9 +335,10 @@ bool FlameCreationTask::createInstance() m_mod_id_resolver = new Flame::FileResolvingTask(APPLICATION->network(), m_pack); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::succeeded, this, [this, &loop] { idResolverSucceeded(loop); }); - connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [&](QString reason) { + connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::failed, [this, &loop](QString reason) { m_mod_id_resolver.reset(); setError(tr("Unable to resolve mod IDs:\n") + reason); + loop.exit(); }); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::progress, this, &FlameCreationTask::setProgress); connect(m_mod_id_resolver.get(), &Flame::FileResolvingTask::status, this, &FlameCreationTask::setStatus); diff --git a/launcher/net/Download.cpp b/launcher/net/Download.cpp index fd3dbedc..a396ed97 100644 --- a/launcher/net/Download.cpp +++ b/launcher/net/Download.cpp @@ -117,11 +117,13 @@ void Download::executeTask() return; } - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host().contains("api.curseforge.com")) { request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); - }; + } + else { + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); + } QNetworkReply* rep = m_network->get(request); diff --git a/launcher/net/FetchFlameAPIKey.cpp b/launcher/net/FetchFlameAPIKey.cpp new file mode 100644 index 00000000..4433f9e9 --- /dev/null +++ b/launcher/net/FetchFlameAPIKey.cpp @@ -0,0 +1,81 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "FetchFlameAPIKey.h" +#include "Application.h" +#include +#include + +#include +#include + +FetchFlameAPIKey::FetchFlameAPIKey(QObject *parent) + : Task{parent} +{ + +} + +void FetchFlameAPIKey::executeTask() +{ + QNetworkRequest req(BuildConfig.FLAME_API_KEY_API_URL); + m_reply.reset(APPLICATION->network()->get(req)); + connect(m_reply.get(), &QNetworkReply::downloadProgress, this, &Task::setProgress); + connect(m_reply.get(), &QNetworkReply::finished, this, &FetchFlameAPIKey::downloadFinished); + connect(m_reply.get(), +#if QT_VERSION >= QT_VERSION_CHECK(5, 15, 0) + &QNetworkReply::errorOccurred, +#else + qOverload(&QNetworkReply::error), +#endif + this, + [this] (QNetworkReply::NetworkError error) { + qCritical() << "Network error: " << error; + emitFailed(m_reply->errorString()); + }); + + setStatus(tr("Fetching Curseforge core API key")); +} + +void FetchFlameAPIKey::downloadFinished() +{ + auto res = m_reply->readAll(); + auto doc = QJsonDocument::fromJson(res); + + qDebug() << doc; + + try { + auto obj = Json::requireObject(doc); + + auto success = Json::requireBoolean(obj, "ok"); + + if (success) + { + m_result = Json::requireString(obj, "token"); + emitSucceeded(); + } + else + { + emitFailed("The API returned an output indicating failure."); + } + } + catch (Json::JsonException&) + { + qCritical() << "Output: " << res; + emitFailed("The API returned an unexpected JSON output."); + } +} diff --git a/launcher/net/FetchFlameAPIKey.h b/launcher/net/FetchFlameAPIKey.h new file mode 100644 index 00000000..6434281a --- /dev/null +++ b/launcher/net/FetchFlameAPIKey.h @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * PolyMC - Minecraft Launcher + * Copyright (C) 2022 Lenny McLennington + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef FETCHFLAMEAPIKEY_H +#define FETCHFLAMEAPIKEY_H + +#include +#include +#include + +class FetchFlameAPIKey : public Task +{ + Q_OBJECT + public: + explicit FetchFlameAPIKey(QObject *parent = nullptr); + + QString m_result; + + public slots: + void downloadFinished(); + + protected: + virtual void executeTask(); + + + std::shared_ptr m_reply; +}; + +#endif // FETCHFLAMEAPIKEY_H diff --git a/launcher/net/Upload.cpp b/launcher/net/Upload.cpp index f3b19022..bed9a3ad 100644 --- a/launcher/net/Upload.cpp +++ b/launcher/net/Upload.cpp @@ -215,11 +215,13 @@ namespace Net { return; } - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); if (APPLICATION->capabilities() & Application::SupportsFlame && request.url().host().contains("api.curseforge.com")) { request.setRawHeader("x-api-key", APPLICATION->getFlameAPIKey().toUtf8()); } + else { + request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgent().toUtf8()); + } //TODO other types of post requests ? request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); QNetworkReply* rep = m_network->post(request, m_post_data); diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index b1ea5ee9..d75e9b37 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -41,6 +41,7 @@ #include #include +#include "net/FetchFlameAPIKey.h" #include "ui/dialogs/ProgressDialog.h" #include "ui/dialogs/CustomMessageBox.h" #include "net/PasteUpload.h" @@ -50,6 +51,31 @@ #include #include +QString GuiUtil::fetchFlameKey(QWidget *parentWidget) +{ + if (BuildConfig.FLAME_API_KEY_API_URL.isEmpty()) + return ""; + + ProgressDialog prog(parentWidget); + auto flameKeyTask = std::make_unique(); + prog.execWithTask(flameKeyTask.get()); + + if (!flameKeyTask->wasSuccessful()) + { + auto message = QObject::tr("Fetching the Curseforge API key failed. Reason: %1").arg(flameKeyTask->failReason()); + if (!(APPLICATION->capabilities() & Application::SupportsFlame)) + { + message += "\n\n" + QObject::tr("Downloading Curseforge modpacks will not work unless you manually set a valid Curseforge Core API key in the settings."); + } + + CustomMessageBox::selectable(parentWidget, + QObject::tr("Failed to fetch Curseforge API key."), + message, QMessageBox::Critical)->exec(); + } + + return flameKeyTask->m_result; +} + QString GuiUtil::uploadPaste(const QString &text, QWidget *parentWidget) { ProgressDialog dialog(parentWidget); diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index 5e109383..dd0102a5 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -4,6 +4,7 @@ namespace GuiUtil { +QString fetchFlameKey(QWidget *parentWidget = nullptr); QString uploadPaste(const QString &text, QWidget *parentWidget); void setClipboardText(const QString &text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget *parentWidget); diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index a0c3b410..681e84b1 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -2,6 +2,7 @@ /* * PolyMC - Minecraft Launcher * Copyright (C) 2022 Sefa Eyeoglu + * Copyright (C) 2022 Lenny McLennington * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by @@ -1512,8 +1513,14 @@ InstanceView void MainWindow::runModalTask(Task *task) { - connect(task, &Task::failed, [this](QString reason) + ProgressDialog loadDialog(this); + + connect(task, &Task::failed, [this, &loadDialog](QString reason) { + // FIXME: + // HACK: I don't know why calling show() on this CustomMessageBox causes loadDialog to not close, + // but this forces it to close BEFORE the CustomMessageBox gets opened... I think this is a bad fix + loadDialog.close(); CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); }); connect(task, &Task::succeeded, [this, task]() @@ -1524,13 +1531,15 @@ void MainWindow::runModalTask(Task *task) CustomMessageBox::selectable(this, tr("Warnings"), warnings.join('\n'), QMessageBox::Warning)->show(); } }); - connect(task, &Task::aborted, [this] + connect(task, &Task::aborted, [this, &loadDialog] { + // HACK: Same bad hack as above slot for Task::failed + loadDialog.close(); CustomMessageBox::selectable(this, tr("Task aborted"), tr("The task has been aborted by the user."), QMessageBox::Information)->show(); }); - ProgressDialog loadDialog(this); loadDialog.setSkipButton(true, tr("Abort")); loadDialog.execWithTask(task); + qDebug() << "MainWindow::runModalTask: execWithTask exited properly"; } void MainWindow::instanceFromInstanceTask(InstanceTask *rawTask) diff --git a/launcher/ui/pages/global/APIPage.cpp b/launcher/ui/pages/global/APIPage.cpp index e3d30475..6ee352c0 100644 --- a/launcher/ui/pages/global/APIPage.cpp +++ b/launcher/ui/pages/global/APIPage.cpp @@ -52,6 +52,8 @@ #include "net/PasteUpload.h" #include "BuildConfig.h" +#include "ui/GuiUtil.h" + APIPage::APIPage(QWidget *parent) : QWidget(parent), ui(new Ui::APIPage) @@ -87,11 +89,16 @@ APIPage::APIPage(QWidget *parent) : ui->metaURL->setPlaceholderText(BuildConfig.META_URL); ui->userAgentLineEdit->setPlaceholderText(BuildConfig.USER_AGENT); + if (BuildConfig.FLAME_API_KEY_API_URL.isEmpty()) + ui->fetchKeyButton->hide(); + loadSettings(); resetBaseURLNote(); connect(ui->pasteTypeComboBox, currentIndexChangedSignal, this, &APIPage::updateBaseURLNote); connect(ui->baseURLEntry, &QLineEdit::textEdited, this, &APIPage::resetBaseURLNote); + + connect(ui->fetchKeyButton, &QPushButton::clicked, this, &APIPage::fetchKeyButtonPressed); } APIPage::~APIPage() @@ -178,6 +185,16 @@ void APIPage::applySettings() QString flameKey = ui->flameKey->text(); s->set("FlameKeyOverride", flameKey); s->set("UserAgentOverride", ui->userAgentLineEdit->text()); + + APPLICATION->updateCapabilities(); +} + +void APIPage::fetchKeyButtonPressed() +{ + QString apiKey = GuiUtil::fetchFlameKey(parentWidget()); + + if (!apiKey.isEmpty()) + ui->flameKey->setText(apiKey); } bool APIPage::apply() diff --git a/launcher/ui/pages/global/APIPage.h b/launcher/ui/pages/global/APIPage.h index 17e62ae7..82d34a9c 100644 --- a/launcher/ui/pages/global/APIPage.h +++ b/launcher/ui/pages/global/APIPage.h @@ -80,6 +80,7 @@ private: void updateBaseURLPlaceholder(int index); void loadSettings(); void applySettings(); + void fetchKeyButtonPressed(); private: Ui::APIPage *ui; diff --git a/launcher/ui/pages/global/APIPage.ui b/launcher/ui/pages/global/APIPage.ui index 1ae788c7..96d1c937 100644 --- a/launcher/ui/pages/global/APIPage.ui +++ b/launcher/ui/pages/global/APIPage.ui @@ -204,13 +204,6 @@ &CurseForge Core API - - - - Note: you probably don't need to set this if CurseForge already works. - - - @@ -237,6 +230,29 @@ + + + + Fetch Official Launcher's Key + + + + + + + + 0 + 0 + + + + <html><head/><body><p>Note: you probably don't need to set this if CurseForge already works.</p><p><span style=" font-weight:700;">Using the Official Curseforge Launcher's key may break Curseforge's Terms of service, but should allow PolyMC to download all mods in a modpack without you needing to download any of them manually.</span></p></body></html> + + + true + + + diff --git a/libraries/katabasis/src/DeviceFlow.cpp b/libraries/katabasis/src/DeviceFlow.cpp index ba1d121d..2465dcf8 100644 --- a/libraries/katabasis/src/DeviceFlow.cpp +++ b/libraries/katabasis/src/DeviceFlow.cpp @@ -230,6 +230,15 @@ void DeviceFlow::onDeviceAuthReplyFinished() qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: Mandatory parameters missing from response"; updateActivity(Activity::FailedHard); } + } else { + qWarning() << "DeviceFlow::onDeviceAuthReplyFinished: Token reply error:" << tokenReply->errorString(); + + QVariantMap params = parseJsonResponse(tokenReply->readAll()); + foreach (QString key, params.keys()) { + qDebug() << "\t" << key << ": " << params.value(key).toString(); + } + + updateActivity(Activity::FailedHard); } tokenReply->deleteLater(); }