Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

CurseForge URL handling #981

Merged
merged 20 commits into from
Aug 17, 2023
Merged
Show file tree
Hide file tree
Changes from 19 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 11 additions & 0 deletions cmake/MacOSXBundleInfo.plist.in
Original file line number Diff line number Diff line change
Expand Up @@ -67,5 +67,16 @@
<string>Alternate</string>
</dict>
</array>
<key>CFBundleURLTypes</key>
<array>
<dict>
<key>CFBundleURLName</key>
<string>Curseforge</string>
<key>CFBundleURLSchemes</key>
<array>
<string>curseforge</string>
</array>
</dict>
</array>
</dict>
</plist>
41 changes: 27 additions & 14 deletions launcher/Application.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -194,8 +194,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
{ { "s", "server" }, "Join the specified server on launch (only valid in combination with --launch)", "address" },
{ { "a", "profile" }, "Use the account specified by its profile name (only valid in combination with --launch)", "profile" },
{ "alive", "Write a small '" + liveCheckFile + "' file after the launcher starts" },
{ { "I", "import" }, "Import instance from specified zip (local path or URL)", "file" },
{ { "I", "import" }, "Import instance or resource from specified local path or URL", "url" },
{ "show", "Opens the window for the specified instance (by instance ID)", "show" } });
// Has to be positional for some OS to handle that properly
parser.addPositionalArgument("URL", "Import the resource(s) at the given URL(s) (same as -I / --import)", "[URL...]");

parser.addHelpOption();
parser.addVersionOption();

Expand All @@ -208,13 +211,13 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)

m_instanceIdToShowWindowOf = parser.value("show");

for (auto zip_path : parser.values("import")) {
m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
for (auto url : parser.values("import")) {
m_urlsToImport.append(normalizeImportUrl(url));
}

// treat unspecified positional arguments as import urls
for (auto zip_path : parser.positionalArguments()) {
m_zipsToImport.append(QUrl::fromLocalFile(QFileInfo(zip_path).absoluteFilePath()));
for (auto url : parser.positionalArguments()) {
m_urlsToImport.append(normalizeImportUrl(url));
}

// error if --launch is missing with --server or --profile
Expand Down Expand Up @@ -313,11 +316,11 @@ Application::Application(int& argc, char** argv) : QApplication(argc, argv)
activate.command = "activate";
m_peerInstance->sendMessage(activate.serialize(), timeout);

if (!m_zipsToImport.isEmpty()) {
for (auto zip_url : m_zipsToImport) {
if (!m_urlsToImport.isEmpty()) {
for (auto url : m_urlsToImport) {
ApplicationMessage import;
import.command = "import";
import.args.insert("path", zip_url.toString());
import.args.insert("url", url.toString());
m_peerInstance->sendMessage(import.serialize(), timeout);
}
}
Expand Down Expand Up @@ -978,9 +981,9 @@ void Application::performMainStartupAction()
showMainWindow(false);
qDebug() << "<> Main window shown.";
}
if (!m_zipsToImport.isEmpty()) {
qDebug() << "<> Importing from zip:" << m_zipsToImport;
m_mainWindow->processURLs(m_zipsToImport);
if (!m_urlsToImport.isEmpty()) {
qDebug() << "<> Importing from url:" << m_urlsToImport;
m_mainWindow->processURLs(m_urlsToImport);
}
}

Expand Down Expand Up @@ -1022,12 +1025,12 @@ void Application::messageReceived(const QByteArray& message)
if (command == "activate") {
showMainWindow();
} else if (command == "import") {
QString path = received.args["path"];
if (path.isEmpty()) {
QString url = received.args["url"];
if (url.isEmpty()) {
qWarning() << "Received" << command << "message without a zip path/URL.";
return;
}
m_mainWindow->processURLs({ QUrl::fromLocalFile(QFileInfo(path).absoluteFilePath()) });
m_mainWindow->processURLs({ normalizeImportUrl(url) });
} else if (command == "launch") {
QString id = received.args["id"];
QString server = received.args["server"];
Expand Down Expand Up @@ -1590,3 +1593,13 @@ void Application::triggerUpdateCheck()
qDebug() << "Updater not available.";
}
}

QUrl Application::normalizeImportUrl(QString const& url)
{
auto local_file = QFileInfo(url);
if (local_file.exists()) {
return QUrl::fromLocalFile(local_file.absoluteFilePath());
} else {
return QUrl::fromUserInput(url);
}
}
4 changes: 3 additions & 1 deletion launcher/Application.h
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,8 @@ class Application : public QApplication {

int suitableMaxMem();

QUrl normalizeImportUrl(QString const& url);

signals:
void updateAllowedChanged(bool status);
void globalSettingsAboutToOpen();
Expand Down Expand Up @@ -279,7 +281,7 @@ class Application : public QApplication {
QString m_serverToJoin;
QString m_profileToUse;
bool m_liveCheck = false;
QList<QUrl> m_zipsToImport;
QList<QUrl> m_urlsToImport;
QString m_instanceIdToShowWindowOf;
std::unique_ptr<QFile> logFile;
};
35 changes: 19 additions & 16 deletions launcher/InstanceImportTask.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@
#include "modplatform/technic/TechnicPackProcessor.h"

#include "settings/INISettingsObject.h"
#include "tasks/Task.h"

#include "net/ApiDownload.h"

Expand Down Expand Up @@ -90,25 +91,27 @@ void InstanceImportTask::executeTask()
setStatus(tr("Downloading modpack:\n%1").arg(m_sourceUrl.toString()));
m_downloadRequired = true;

const QString path(m_sourceUrl.host() + '/' + m_sourceUrl.path());

auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true);
m_archivePath = entry->getFullPath();

m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));

connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);

m_filesNetJob->start();
downloadFromUrl();
}
}

void InstanceImportTask::downloadFromUrl()
{
const QString path = m_sourceUrl.host() + '/' + m_sourceUrl.path();
auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true);
m_filesNetJob.reset(new NetJob(tr("Modpack download"), APPLICATION->network()));
m_filesNetJob->addNetAction(Net::ApiDownload::makeCached(m_sourceUrl, entry));
m_archivePath = entry->getFullPath();

connect(m_filesNetJob.get(), &NetJob::succeeded, this, &InstanceImportTask::downloadSucceeded);
connect(m_filesNetJob.get(), &NetJob::progress, this, &InstanceImportTask::downloadProgressChanged);
connect(m_filesNetJob.get(), &NetJob::stepProgress, this, &InstanceImportTask::propagateStepProgress);
connect(m_filesNetJob.get(), &NetJob::failed, this, &InstanceImportTask::downloadFailed);
connect(m_filesNetJob.get(), &NetJob::aborted, this, &InstanceImportTask::downloadAborted);
m_filesNetJob->start();
}

void InstanceImportTask::downloadSucceeded()
{
processZipPack();
Expand Down
1 change: 1 addition & 0 deletions launcher/InstanceImportTask.h
Original file line number Diff line number Diff line change
Expand Up @@ -101,4 +101,5 @@ class InstanceImportTask : public InstanceTask {

// FIXME: nuke
QWidget* m_parent;
void downloadFromUrl();
};
11 changes: 11 additions & 0 deletions launcher/modplatform/flame/FlameAPI.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,17 @@ Task::Ptr FlameAPI::getFiles(const QStringList& fileIds, std::shared_ptr<QByteAr
return netJob;
}

Task::Ptr FlameAPI::getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray>response) const
{
auto netJob = makeShared<NetJob>(QString("Flame::GetFile"), APPLICATION->network());
netJob->addNetAction(
Net::ApiDownload::makeByteArray(QUrl(QString("https://api.curseforge.com/v1/mods/%1/files/%2").arg(addonId, fileId)), response));

QObject::connect(netJob.get(), &NetJob::failed, [addonId, fileId] { qDebug() << "Flame API file failure" << addonId << fileId; });

return netJob;
}

// https://docs.curseforge.com/?python#tocS_ModsSearchSortField
static QList<ResourceAPI::SortingMethod> s_sorts = { { 1, "Featured", QObject::tr("Sort by Featured") },
{ 2, "Popularity", QObject::tr("Sort by Popularity") },
Expand Down
1 change: 1 addition & 0 deletions launcher/modplatform/flame/FlameAPI.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ class FlameAPI : public NetworkResourceAPI {
Task::Ptr getProjects(QStringList addonIds, std::shared_ptr<QByteArray> response) const override;
Task::Ptr matchFingerprints(const QList<uint>& fingerprints, std::shared_ptr<QByteArray> response);
Task::Ptr getFiles(const QStringList& fileIds, std::shared_ptr<QByteArray> response) const;
Task::Ptr getFile(const QString& addonId, const QString& fileId, std::shared_ptr<QByteArray> response) const;

[[nodiscard]] auto getSortingMethods() const -> QList<ResourceAPI::SortingMethod> override;

Expand Down
103 changes: 95 additions & 8 deletions launcher/ui/MainWindow.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@
#include <launch/LaunchTask.h>
#include <minecraft/MinecraftInstance.h>
#include <minecraft/auth/AccountList.h>
#include <net/Download.h>
#include <net/ApiDownload.h>
#include <net/NetJob.h>
#include <news/NewsChecker.h>
#include <tools/BaseProfiler.h>
Expand Down Expand Up @@ -118,11 +118,15 @@
#include "minecraft/mod/ShaderPackFolderModel.h"
#include "minecraft/mod/tasks/LocalResourceParse.h"

#include "modplatform/flame/FlameAPI.h"

#include "KonamiCode.h"

#include "InstanceCopyTask.h"
#include "InstanceImportTask.h"

#include "Json.h"

#include "MMCTime.h"

namespace {
Expand Down Expand Up @@ -929,7 +933,7 @@ void MainWindow::finalizeInstance(InstancePtr inst)
}
}

void MainWindow::addInstance(QString url)
void MainWindow::addInstance(const QString& url, const QMap<QString, QString>& extra_info)
{
QString groupName;
do {
Expand All @@ -949,7 +953,7 @@ void MainWindow::addInstance(QString url)
groupName = APPLICATION->settings()->get("LastUsedGroupForNewInstance").toString();
}

NewInstanceDialog newInstDlg(groupName, url, this);
NewInstanceDialog newInstDlg(groupName, url, extra_info, this);
if (!newInstDlg.exec())
return;

Expand All @@ -976,18 +980,101 @@ void MainWindow::processURLs(QList<QUrl> urls)
if (url.scheme().isEmpty())
url.setScheme("file");

if (!url.isLocalFile()) { // probably instance/modpack
addInstance(url.toString());
break;
QMap<QString, QString> extra_info;
QUrl local_url;
if (!url.isLocalFile()) { // download the remote resource and identify
QUrl dl_url;
if(url.scheme() == "curseforge") {
// need to find the download link for the modpack / resource
// format of url curseforge://install?addonId=IDHERE&fileId=IDHERE
QUrlQuery query(url);

auto addonId = query.allQueryItemValues("addonId")[0];
auto fileId = query.allQueryItemValues("fileId")[0];

extra_info.insert("pack_id", addonId);
extra_info.insert("pack_version_id", fileId);

auto array = std::make_shared<QByteArray>();

auto api = FlameAPI();
auto job = api.getFile(addonId, fileId, array);

QString resource_name;

connect(job.get(), &Task::failed, this,
[this](QString reason) { CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(job.get(), &Task::succeeded, this, [this, array, addonId, fileId, &dl_url, &resource_name] {
qDebug() << "Returned CFURL Json:\n" << array->toStdString().c_str();
auto doc = Json::requireDocument(*array);
auto data = Json::ensureObject(Json::ensureObject(doc.object()), "data");
// No way to find out if it's a mod or a modpack before here
// And also we need to check if it ends with .zip, instead of any better way
auto fileName = Json::ensureString(data, "fileName");

// Have to use ensureString then use QUrl to get proper url encoding
dl_url = QUrl(Json::ensureString(data, "downloadUrl", "", "downloadUrl"));
if (!dl_url.isValid()) {
CustomMessageBox::selectable(
this, tr("Error"),
tr("The modpack, mod, or resource %1 is blocked for third-parties! Please download it manually.").arg(fileName),
QMessageBox::Critical)
->show();
return;
}

QFileInfo dl_file(dl_url.fileName());
resource_name = Json::ensureString(data, "displayName", dl_file.completeBaseName(), "displayName");
});

{ // drop stack
ProgressDialog dlUrlDialod(this);
dlUrlDialod.setSkipButton(true, tr("Abort"));
dlUrlDialod.execWithTask(job.get());
}


} else {
dl_url = url;
}

if (!dl_url.isValid()) {
continue; // no valid url to download this resource
}

const QString path = dl_url.host() + '/' + dl_url.path();
auto entry = APPLICATION->metacache()->resolveEntry("general", path);
entry->setStale(true);
auto dl_job = unique_qobject_ptr<NetJob>(new NetJob(tr("Modpack download"), APPLICATION->network()));
dl_job->addNetAction(Net::ApiDownload::makeCached(dl_url, entry));
auto archivePath = entry->getFullPath();

bool dl_success = false;
connect(dl_job.get(), &Task::failed, this, [this](QString reason){CustomMessageBox::selectable(this, tr("Error"), reason, QMessageBox::Critical)->show(); });
connect(dl_job.get(), &Task::succeeded, this, [&dl_success]{dl_success = true;});

{ // drop stack
ProgressDialog dlUrlDialod(this);
dlUrlDialod.setSkipButton(true, tr("Abort"));
dlUrlDialod.execWithTask(dl_job.get());
}

if (!dl_success) {
continue; // no local file to identify
}
local_url = QUrl::fromLocalFile(archivePath);

} else {
local_url = url;
}

auto localFileName = QDir::toNativeSeparators(url.toLocalFile());
auto localFileName = QDir::toNativeSeparators(local_url.toLocalFile());
QFileInfo localFileInfo(localFileName);

auto type = ResourceUtils::identify(localFileInfo);

if (ResourceUtils::ValidResourceTypes.count(type) == 0) { // probably instance/modpack
addInstance(localFileName);
addInstance(localFileName, extra_info);
continue;
}

Expand Down
2 changes: 1 addition & 1 deletion launcher/ui/MainWindow.h
Original file line number Diff line number Diff line change
Expand Up @@ -220,7 +220,7 @@ class MainWindow : public QMainWindow {
private:
void retranslateUi();

void addInstance(QString url = QString());
void addInstance(const QString& url = QString(), const QMap<QString, QString>& extra_info = {});
void activateInstance(InstancePtr instance);
void setCatBackground(bool enabled);
void updateInstanceToolIcon(QString new_icon);
Expand Down
7 changes: 5 additions & 2 deletions launcher/ui/dialogs/NewInstanceDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,10 @@
#include "ui/pages/modplatform/modrinth/ModrinthPage.h"
#include "ui/pages/modplatform/technic/TechnicPage.h"
#include "ui/widgets/PageContainer.h"

NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, const QString& url, QWidget* parent)
NewInstanceDialog::NewInstanceDialog(const QString& initialGroup,
const QString& url,
const QMap<QString, QString>& extra_info,
QWidget* parent)
: QDialog(parent), ui(new Ui::NewInstanceDialog)
{
ui->setupUi(this);
Expand Down Expand Up @@ -125,6 +127,7 @@ NewInstanceDialog::NewInstanceDialog(const QString& initialGroup, const QString&
QUrl actualUrl(url);
m_container->selectPage("import");
importPage->setUrl(url);
importPage->setExtraInfo(extra_info);
}

updateDialogState();
Expand Down
5 changes: 4 additions & 1 deletion launcher/ui/dialogs/NewInstanceDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,10 @@ class NewInstanceDialog : public QDialog, public BasePageProvider {
Q_OBJECT

public:
explicit NewInstanceDialog(const QString& initialGroup, const QString& url = QString(), QWidget* parent = 0);
explicit NewInstanceDialog(const QString& initialGroup,
const QString& url = QString(),
const QMap<QString, QString>& extra_info = {},
QWidget* parent = 0);
~NewInstanceDialog();

void updateDialogState();
Expand Down