summaryrefslogtreecommitdiff
path: root/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp')
-rw-r--r--archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp393
1 files changed, 393 insertions, 0 deletions
diff --git a/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp b/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp
new file mode 100644
index 0000000000..324f2d41d9
--- /dev/null
+++ b/archived/projt-launcher/launcher/modplatform/ResourceAPI.cpp
@@ -0,0 +1,393 @@
+// SPDX-License-Identifier: GPL-3.0-only
+// SPDX-FileCopyrightText: 2026 Project Tick
+// SPDX-FileContributor: Project Tick Team
+/*
+ * ProjT Launcher - Minecraft Launcher
+ * Copyright (C) 2026 Project Tick
+ *
+ * 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, write to the Free Software Foundation,
+ * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
+ */
+#include "modplatform/ResourceAPI.h"
+
+#include "Application.h"
+#include "Json.h"
+#include "net/NetJob.h"
+
+#include "modplatform/ModIndex.h"
+
+#include "net/ApiDownload.h"
+
+Task::Ptr ResourceAPI::searchProjects(SearchArgs&& args,
+ Callback<QList<ModPlatform::IndexedPack::Ptr>>&& callbacks) const
+{
+ auto search_url_optional = getSearchURL(args);
+ if (!search_url_optional.has_value())
+ {
+ callbacks.on_fail("Failed to create search URL", -1);
+ return nullptr;
+ }
+
+ auto search_url = search_url_optional.value();
+
+ auto response = std::make_shared<QByteArray>();
+ auto netJob = makeShared<NetJob>(QString("%1::Search").arg(debugName()), APPLICATION->network());
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(search_url), response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response from " << debugName() << " at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+
+ callbacks.on_fail(parse_error.errorString(), -1);
+
+ return;
+ }
+
+ QList<ModPlatform::IndexedPack::Ptr> newList;
+ auto packs = documentToArray(doc);
+
+ for (auto packRaw : packs)
+ {
+ auto packObj = packRaw.toObject();
+
+ ModPlatform::IndexedPack::Ptr pack = std::make_shared<ModPlatform::IndexedPack>();
+ try
+ {
+ loadIndexedPack(*pack, packObj);
+ newList << pack;
+ }
+ catch (const JSONValidationError& e)
+ {
+ qWarning() << "Error while loading resource from " << debugName() << ": " << e.cause();
+ continue;
+ }
+ }
+
+ callbacks.on_succeed(newList);
+ });
+
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = netJob.toWeakRef();
+ QObject::connect(netJob.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto netJob = weak.lock())
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ network_error_code = failed_action->replyStatusCode();
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(netJob.get(),
+ &NetJob::aborted,
+ [callbacks]
+ {
+ if (callbacks.on_abort != nullptr)
+ callbacks.on_abort();
+ });
+
+ return netJob;
+}
+
+Task::Ptr ResourceAPI::getProjectVersions(VersionSearchArgs&& args,
+ Callback<QVector<ModPlatform::IndexedVersion>>&& callbacks) const
+{
+ auto versions_url_optional = getVersionsURL(args);
+ if (!versions_url_optional.has_value())
+ return nullptr;
+
+ auto versions_url = versions_url_optional.value();
+
+ auto netJob = makeShared<NetJob>(QString("%1::Versions").arg(args.pack->name), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks, args]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response for getting versions at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ QVector<ModPlatform::IndexedVersion> unsortedVersions;
+ try
+ {
+ auto arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+
+ for (auto versionIter : arr)
+ {
+ auto obj = versionIter.toObject();
+
+ auto file = loadIndexedPackVersion(obj, args.resourceType);
+ if (!file.addonId.isValid())
+ file.addonId = args.pack->addonId;
+
+ if (file.fileId.isValid() && !file.downloadUrl.isEmpty()) // Heuristic to check if the
+ // returned value is valid
+ unsortedVersions.append(file);
+ }
+
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a,
+ const ModPlatform::IndexedVersion& b) -> bool
+ {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(unsortedVersions.begin(), unsortedVersions.end(), orderSortPredicate);
+ }
+ catch (const JSONValidationError& e)
+ {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource version: " << e.cause();
+ }
+
+ callbacks.on_succeed(unsortedVersions);
+ });
+
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = netJob.toWeakRef();
+ QObject::connect(netJob.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto netJob = weak.lock())
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ network_error_code = failed_action->replyStatusCode();
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(netJob.get(),
+ &NetJob::aborted,
+ [callbacks]
+ {
+ if (callbacks.on_abort != nullptr)
+ callbacks.on_abort();
+ });
+
+ return netJob;
+}
+
+Task::Ptr ResourceAPI::getProjectInfo(ProjectInfoArgs&& args, Callback<ModPlatform::IndexedPack::Ptr>&& callbacks) const
+{
+ auto response = std::make_shared<QByteArray>();
+ auto job = getProject(args.pack->addonId.toString(), response);
+
+ QObject::connect(job.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks, args]
+ {
+ auto pack = args.pack;
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response for mod info at " << parse_error.offset
+ << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+ try
+ {
+ auto obj = Json::requireObject(doc);
+ if (obj.contains("data"))
+ obj = Json::requireObject(obj, "data");
+ loadIndexedPack(*pack, obj);
+ loadExtraPackInfo(*pack, obj);
+ }
+ catch (const JSONValidationError& e)
+ {
+ qDebug() << doc;
+ qWarning() << "Error while reading " << debugName() << " resource info: " << e.cause();
+ }
+ callbacks.on_succeed(pack);
+ });
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = job.toWeakRef();
+ QObject::connect(job.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto job = weak.lock())
+ {
+ if (auto netJob = qSharedPointerDynamicCast<NetJob>(job))
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ {
+ network_error_code = failed_action->replyStatusCode();
+ }
+ }
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ QObject::connect(job.get(),
+ &NetJob::aborted,
+ [callbacks]
+ {
+ if (callbacks.on_abort != nullptr)
+ callbacks.on_abort();
+ });
+ return job;
+}
+
+Task::Ptr ResourceAPI::getDependencyVersion(DependencySearchArgs&& args,
+ Callback<ModPlatform::IndexedVersion>&& callbacks) const
+{
+ auto versions_url_optional = getDependencyURL(args);
+ if (!versions_url_optional.has_value())
+ return nullptr;
+
+ auto versions_url = versions_url_optional.value();
+
+ auto netJob =
+ makeShared<NetJob>(QString("%1::Dependency").arg(args.dependency.addonId.toString()), APPLICATION->network());
+ auto response = std::make_shared<QByteArray>();
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(versions_url, response));
+
+ QObject::connect(netJob.get(),
+ &NetJob::succeeded,
+ [this, response, callbacks, args]
+ {
+ QJsonParseError parse_error{};
+ QJsonDocument doc = QJsonDocument::fromJson(*response, &parse_error);
+ if (parse_error.error != QJsonParseError::NoError)
+ {
+ qWarning() << "Error while parsing JSON response for getting versions at "
+ << parse_error.offset << " reason: " << parse_error.errorString();
+ qWarning() << *response;
+ return;
+ }
+
+ QJsonArray arr;
+ if (args.dependency.version.length() != 0 && doc.isObject())
+ {
+ arr.append(doc.object());
+ }
+ else
+ {
+ arr = doc.isObject() ? Json::ensureArray(doc.object(), "data") : doc.array();
+ }
+
+ QVector<ModPlatform::IndexedVersion> versions;
+ for (auto versionIter : arr)
+ {
+ auto obj = versionIter.toObject();
+
+ auto file = loadIndexedPackVersion(obj, ModPlatform::ResourceType::Mod);
+ if (!file.addonId.isValid())
+ file.addonId = args.dependency.addonId;
+
+ if (file.fileId.isValid() && (!file.loaders || args.loader & file.loaders)) // Heuristic to
+ // check if the
+ // returned
+ // value is
+ // valid
+ versions.append(file);
+ }
+
+ auto orderSortPredicate = [](const ModPlatform::IndexedVersion& a,
+ const ModPlatform::IndexedVersion& b) -> bool
+ {
+ // dates are in RFC 3339 format
+ return a.date > b.date;
+ };
+ std::sort(versions.begin(), versions.end(), orderSortPredicate);
+ auto bestMatch = versions.size() != 0 ? versions.front() : ModPlatform::IndexedVersion();
+ callbacks.on_succeed(bestMatch);
+ });
+
+ // Capture a weak_ptr instead of a shared_ptr to avoid circular dependency issues.
+ // This prevents the lambda from extending the lifetime of the shared resource,
+ // as it only temporarily locks the resource when needed.
+ auto weak = netJob.toWeakRef();
+ QObject::connect(netJob.get(),
+ &NetJob::failed,
+ [weak, callbacks](const QString& reason)
+ {
+ int network_error_code = -1;
+ if (auto netJob = weak.lock())
+ {
+ if (auto* failed_action = netJob->getFailedActions().at(0); failed_action)
+ network_error_code = failed_action->replyStatusCode();
+ }
+ callbacks.on_fail(reason, network_error_code);
+ });
+ return netJob;
+}
+
+QString ResourceAPI::getGameVersionsString(std::list<Version> mcVersions) const
+{
+ QString s;
+ for (auto& ver : mcVersions)
+ {
+ s += QString("\"%1\",").arg(mapMCVersionToModrinth(ver));
+ }
+ s.remove(s.length() - 1, 1); // remove last comma
+ return s;
+}
+
+QString ResourceAPI::mapMCVersionToModrinth(Version v) const
+{
+ static const QString preString = " Pre-Release ";
+ auto verStr = v.toString();
+
+ if (verStr.contains(preString))
+ {
+ verStr.replace(preString, "-pre");
+ }
+ verStr.replace(" ", "-");
+ return verStr;
+}
+
+Task::Ptr ResourceAPI::getProject(QString addonId, std::shared_ptr<QByteArray> response) const
+{
+ auto project_url_optional = getInfoURL(addonId);
+ if (!project_url_optional.has_value())
+ return nullptr;
+
+ auto project_url = project_url_optional.value();
+
+ auto netJob = makeShared<NetJob>(QString("%1::GetProject").arg(addonId), APPLICATION->network());
+
+ netJob->addNetAction(Net::ApiDownload::makeByteArray(QUrl(project_url), response));
+
+ return netJob;
+}