diff --git a/buildconfig/BuildConfig.cpp.in b/buildconfig/BuildConfig.cpp.in index 6bebcb80e..3637e7369 100644 --- a/buildconfig/BuildConfig.cpp.in +++ b/buildconfig/BuildConfig.cpp.in @@ -53,7 +53,6 @@ Config::Config() LAUNCHER_SVGFILENAME = "@Launcher_SVGFileName@"; USER_AGENT = "@Launcher_UserAgent@"; - USER_AGENT_UNCACHED = USER_AGENT + " (Uncached)"; // Version information VERSION_MAJOR = @Launcher_VERSION_MAJOR@; diff --git a/buildconfig/BuildConfig.h b/buildconfig/BuildConfig.h index b59adcb57..10c38e3d6 100644 --- a/buildconfig/BuildConfig.h +++ b/buildconfig/BuildConfig.h @@ -107,9 +107,6 @@ class Config { /// User-Agent to use. QString USER_AGENT; - /// User-Agent to use for uncached requests. - QString USER_AGENT_UNCACHED; - /// The git commit hash of this build QString GIT_COMMIT; diff --git a/launcher/Application.cpp b/launcher/Application.cpp index 0daab026c..772e685eb 100644 --- a/launcher/Application.cpp +++ b/launcher/Application.cpp @@ -1883,17 +1883,6 @@ QString Application::getUserAgent() return BuildConfig.USER_AGENT; } -QString Application::getUserAgentUncached() -{ - QString uaOverride = m_settings->get("UserAgentOverride").toString(); - if (!uaOverride.isEmpty()) { - uaOverride += " (Uncached)"; - return uaOverride.replace("$LAUNCHER_VER", BuildConfig.printableVersionString()); - } - - return BuildConfig.USER_AGENT_UNCACHED; -} - bool Application::handleDataMigration(const QString& currentData, const QString& oldData, const QString& name, diff --git a/launcher/Application.h b/launcher/Application.h index 12f41509c..fefb32292 100644 --- a/launcher/Application.h +++ b/launcher/Application.h @@ -160,7 +160,6 @@ class Application : public QApplication { QString getFlameAPIKey(); QString getModrinthAPIToken(); QString getUserAgent(); - QString getUserAgentUncached(); /// this is the root of the 'installation'. Used for automatic updates const QString& root() { return m_rootPath; } diff --git a/launcher/CMakeLists.txt b/launcher/CMakeLists.txt index 4f8b9018a..dbd9dc0bb 100644 --- a/launcher/CMakeLists.txt +++ b/launcher/CMakeLists.txt @@ -834,6 +834,10 @@ SET(LAUNCHER_SOURCES icons/IconList.h icons/IconList.cpp + # log utils + logs/AnonymizeLog.cpp + logs/AnonymizeLog.h + # GUI - windows ui/GuiUtil.h ui/GuiUtil.cpp diff --git a/launcher/logs/AnonymizeLog.cpp b/launcher/logs/AnonymizeLog.cpp new file mode 100644 index 000000000..e5021a616 --- /dev/null +++ b/launcher/logs/AnonymizeLog.cpp @@ -0,0 +1,68 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "AnonymizeLog.h" + +#include + +struct RegReplace { + RegReplace(QRegularExpression r, QString w) : reg(r), with(w) { reg.optimize(); } + QRegularExpression reg; + QString with; +}; + +static const QVector anonymizeRules = { + RegReplace(QRegularExpression("C:\\\\Users\\\\([^\\\\]+)\\\\", QRegularExpression::CaseInsensitiveOption), + "C:\\Users\\********\\"), // windows + RegReplace(QRegularExpression("C:\\/Users\\/([^\\/]+)\\/", QRegularExpression::CaseInsensitiveOption), + "C:/Users/********/"), // windows with forward slashes + RegReplace(QRegularExpression("(?)"), // SESSION_TOKEN + RegReplace(QRegularExpression("new refresh token: \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "new refresh token: \"\""), // refresh token + RegReplace(QRegularExpression("\"device_code\" : \"[^\"]+\"", QRegularExpression::CaseInsensitiveOption), + "\"device_code\" : \"\""), // device code +}; + +void anonymizeLog(QString& log) +{ + for (auto rule : anonymizeRules) { + log.replace(rule.reg, rule.with); + } +} \ No newline at end of file diff --git a/launcher/logs/AnonymizeLog.h b/launcher/logs/AnonymizeLog.h new file mode 100644 index 000000000..2409ecee7 --- /dev/null +++ b/launcher/logs/AnonymizeLog.h @@ -0,0 +1,40 @@ +// SPDX-License-Identifier: GPL-3.0-only +/* + * Prism Launcher - Minecraft Launcher + * Copyright (c) 2025 Trial97 + * + * 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 . + * + * This file incorporates work covered by the following copyright and + * permission notice: + * + * Copyright 2013-2021 MultiMC Contributors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#pragma once + +#include + +void anonymizeLog(QString& log); \ No newline at end of file diff --git a/launcher/net/PasteUpload.cpp b/launcher/net/PasteUpload.cpp index 86a44669e..2333a2256 100644 --- a/launcher/net/PasteUpload.cpp +++ b/launcher/net/PasteUpload.cpp @@ -36,74 +36,44 @@ */ #include "PasteUpload.h" -#include "Application.h" -#include "BuildConfig.h" -#include -#include #include #include #include #include +#include #include +#include "logs/AnonymizeLog.h" -#include "net/Logging.h" +const std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, + { "hastebin", "https://hst.sh", "/documents" }, + { "paste.gg", "https://paste.gg", "/api/v1/pastes" }, + { "mclo.gs", "https://api.mclo.gs", "/1/log" } } }; -std::array PasteUpload::PasteTypes = { { { "0x0.st", "https://0x0.st", "" }, - { "hastebin", "https://hst.sh", "/documents" }, - { "paste.gg", "https://paste.gg", "/api/v1/pastes" }, - { "mclo.gs", "https://api.mclo.gs", "/1/log" } } }; - -PasteUpload::PasteUpload(QWidget* window, QString text, QString baseUrl, PasteType pasteType) - : m_window(window), m_baseUrl(baseUrl), m_pasteType(pasteType), m_text(text.toUtf8()) +QNetworkReply* PasteUpload::getReply(QNetworkRequest& request) { - if (m_baseUrl == "") - m_baseUrl = PasteTypes.at(pasteType).defaultBase; - - // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? - if (pasteType == PasteGG && m_baseUrl == PasteTypes.at(pasteType).defaultBase) - m_uploadUrl = "https://api.paste.gg/v1/pastes"; - else - m_uploadUrl = m_baseUrl + PasteTypes.at(pasteType).endpointPath; -} - -PasteUpload::~PasteUpload() {} - -void PasteUpload::executeTask() -{ - QNetworkRequest request{ QUrl(m_uploadUrl) }; - QNetworkReply* rep{}; - - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); - - switch (m_pasteType) { - case NullPointer: { - QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType }; + switch (m_paste_type) { + case PasteUpload::NullPointer: { + QHttpMultiPart* multiPart = new QHttpMultiPart{ QHttpMultiPart::FormDataType, this }; QHttpPart filePart; - filePart.setBody(m_text); + filePart.setBody(m_log.toUtf8()); filePart.setHeader(QNetworkRequest::ContentTypeHeader, "text/plain"); filePart.setHeader(QNetworkRequest::ContentDispositionHeader, "form-data; name=\"file\"; filename=\"log.txt\""); multiPart->append(filePart); - rep = APPLICATION->network()->post(request, multiPart); - multiPart->setParent(rep); - - break; + return m_network->post(request, multiPart); } - case Hastebin: { - request.setHeader(QNetworkRequest::UserAgentHeader, APPLICATION->getUserAgentUncached().toUtf8()); - rep = APPLICATION->network()->post(request, m_text); - break; + case PasteUpload::Hastebin: { + return m_network->post(request, m_log.toUtf8()); } - case Mclogs: { + case PasteUpload::Mclogs: { QUrlQuery postData; - postData.addQueryItem("content", m_text); + postData.addQueryItem("content", m_log); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); - rep = APPLICATION->network()->post(request, postData.toString().toUtf8()); - break; + return m_network->post(request, postData.toString().toUtf8()); } - case PasteGG: { + case PasteUpload::PasteGG: { QJsonObject obj; QJsonDocument doc; request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json"); @@ -114,7 +84,7 @@ void PasteUpload::executeTask() QJsonObject logFileInfo; QJsonObject logFileContentInfo; logFileContentInfo.insert("format", "text"); - logFileContentInfo.insert("value", QString::fromUtf8(m_text)); + logFileContentInfo.insert("value", m_log); logFileInfo.insert("name", "log.txt"); logFileInfo.insert("content", logFileContentInfo); files.append(logFileInfo); @@ -122,108 +92,117 @@ void PasteUpload::executeTask() obj.insert("files", files); doc.setObject(obj); - rep = APPLICATION->network()->post(request, doc.toJson()); - break; + return m_network->post(request, doc.toJson()); } } - connect(rep, &QNetworkReply::uploadProgress, this, &Task::setProgress); - connect(rep, &QNetworkReply::finished, this, &PasteUpload::downloadFinished); + return nullptr; +}; - connect(rep, &QNetworkReply::errorOccurred, this, &PasteUpload::downloadError); +auto PasteUpload::Sink::init(QNetworkRequest&) -> Task::State +{ + m_output.clear(); + return Task::State::Running; +}; - m_reply = std::shared_ptr(rep); - - setStatus(tr("Uploading to %1").arg(m_uploadUrl)); +auto PasteUpload::Sink::write(QByteArray& data) -> Task::State +{ + m_output.append(data); + return Task::State::Running; } -void PasteUpload::downloadError(QNetworkReply::NetworkError error) +auto PasteUpload::Sink::abort() -> Task::State { - // error happened during download. - qCCritical(taskUploadLogC) << getUid().toString() << "Network error: " << error; - emitFailed(m_reply->errorString()); + m_output.clear(); + return Task::State::Failed; } -void PasteUpload::downloadFinished() +auto PasteUpload::Sink::finalize(QNetworkReply&) -> Task::State { - QByteArray data = m_reply->readAll(); - int statusCode = m_reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); - - if (m_reply->error() != QNetworkReply::NetworkError::NoError) { - emitFailed(tr("Network error: %1").arg(m_reply->errorString())); - m_reply.reset(); - return; - } else if (statusCode != 200 && statusCode != 201) { - QString reasonPhrase = m_reply->attribute(QNetworkRequest::HttpReasonPhraseAttribute).toString(); - emitFailed(tr("Error: %1 returned unexpected status code %2 %3").arg(m_uploadUrl).arg(statusCode).arg(reasonPhrase)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned unexpected status code " << statusCode - << " with body: " << data; - m_reply.reset(); - return; - } - - switch (m_pasteType) { - case NullPointer: - m_pasteLink = QString::fromUtf8(data).trimmed(); + switch (m_paste_type) { + case PasteUpload::NullPointer: + m_result->link = QString::fromUtf8(m_output).trimmed(); break; - case Hastebin: { - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("key") && jsonObj["key"].isString()) { - QString key = jsonDoc.object()["key"].toString(); - m_pasteLink = m_baseUrl + "/" + key; + case PasteUpload::Hastebin: { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "hastebin server did not reply with JSON" << jsonError.errorString(); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("key") && obj["key"].isString()) { + QString key = doc.object()["key"].toString(); + m_result->link = m_base_url + "/" + key; } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << getUid().toString() << m_uploadUrl - << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + return Task::State::Failed; } break; } - case Mclogs: { - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("success") && jsonObj["success"].isBool()) { - bool success = jsonObj["success"].toBool(); + case PasteUpload::Mclogs: { + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "mclogs server did not reply with JSON" << jsonError.errorString(); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("success") && obj["success"].isBool()) { + bool success = obj["success"].toBool(); if (success) { - m_pasteLink = jsonObj["url"].toString(); + m_result->link = obj["url"].toString(); } else { - QString error = jsonObj["error"].toString(); - emitFailed(tr("Error: %1 returned an error: %2").arg(m_uploadUrl, error)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; - qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data; - return; + m_result->error = obj["error"].toString(); } } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + return Task::State::Failed; } break; } - case PasteGG: - QJsonDocument jsonDoc{ QJsonDocument::fromJson(data) }; - QJsonObject jsonObj{ jsonDoc.object() }; - if (jsonObj.contains("status") && jsonObj["status"].isString()) { - QString status = jsonObj["status"].toString(); + case PasteUpload::PasteGG: + QJsonParseError jsonError; + auto doc = QJsonDocument::fromJson(m_output, &jsonError); + if (jsonError.error != QJsonParseError::NoError) { + qDebug() << "pastegg server did not reply with JSON" << jsonError.errorString(); + return Task::State::Failed; + } + auto obj = doc.object(); + if (obj.contains("status") && obj["status"].isString()) { + QString status = obj["status"].toString(); if (status == "success") { - m_pasteLink = m_baseUrl + "/p/anonymous/" + jsonObj["result"].toObject()["id"].toString(); + m_result->link = m_base_url + "/p/anonymous/" + obj["result"].toObject()["id"].toString(); } else { - QString error = jsonObj["error"].toString(); - QString message = - (jsonObj.contains("message") && jsonObj["message"].isString()) ? jsonObj["message"].toString() : "none"; - emitFailed(tr("Error: %1 returned an error code: %2\nError message: %3").arg(m_uploadUrl, error, message)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned error: " << error; - qCCritical(taskUploadLogC) << getUid().toString() << "Error message: " << message; - qCCritical(taskUploadLogC) << getUid().toString() << "Response body: " << data; - return; + m_result->error = obj["error"].toString(); + m_result->extra_message = (obj.contains("message") && obj["message"].isString()) ? obj["message"].toString() : "none"; } } else { - emitFailed(tr("Error: %1 returned a malformed response body").arg(m_uploadUrl)); - qCCritical(taskUploadLogC) << getUid().toString() << m_uploadUrl << " returned malformed response body: " << data; - return; + qDebug() << "Log upload failed:" << doc.toJson(); + return Task::State::Failed; } break; } - emitSucceeded(); + return Task::State::Succeeded; +} + +Net::NetRequest::Ptr PasteUpload::make(const QString& log, PasteUpload::PasteType pasteType, QString customBaseURL, ResultPtr result) +{ + auto base = PasteUpload::PasteTypes.at(pasteType); + QString baseUrl = customBaseURL.isEmpty() ? base.defaultBase : customBaseURL; + auto up = makeShared(log, pasteType); + + // HACK: Paste's docs say the standard API path is at /api/ but the official instance paste.gg doesn't follow that?? + if (pasteType == PasteUpload::PasteGG && baseUrl == base.defaultBase) + up->m_url = "https://api.paste.gg/v1/pastes"; + else + up->m_url = baseUrl + base.endpointPath; + + up->m_sink.reset(new Sink(pasteType, baseUrl, result)); + return up; +} + +PasteUpload::PasteUpload(const QString& log, PasteType pasteType) : m_log(log), m_paste_type(pasteType) +{ + anonymizeLog(m_log); } diff --git a/launcher/net/PasteUpload.h b/launcher/net/PasteUpload.h index 2ba6067c3..55fb2231c 100644 --- a/launcher/net/PasteUpload.h +++ b/launcher/net/PasteUpload.h @@ -35,15 +35,17 @@ #pragma once -#include -#include -#include -#include -#include +#include "net/NetRequest.h" #include "tasks/Task.h" -class PasteUpload : public Task { - Q_OBJECT +#include +#include +#include + +#include +#include + +class PasteUpload : public Net::NetRequest { public: enum PasteType : int { // 0x0.st @@ -58,32 +60,47 @@ class PasteUpload : public Task { First = NullPointer, Last = Mclogs }; - struct PasteTypeInfo { const QString name; const QString defaultBase; const QString endpointPath; }; - static std::array PasteTypes; + static const std::array PasteTypes; + struct Result { + QString link; + QString error; + QString extra_message; + }; - PasteUpload(QWidget* window, QString text, QString url, PasteType pasteType); - virtual ~PasteUpload(); + using ResultPtr = std::shared_ptr; - QString pasteLink() { return m_pasteLink; } + class Sink : public Net::Sink { + public: + Sink(const PasteType pasteType, const QString base_url, ResultPtr result) + : m_paste_type(pasteType), m_base_url(base_url), m_result(result) {}; + virtual ~Sink() = default; - protected: - virtual void executeTask(); + public: + auto init(QNetworkRequest& request) -> Task::State override; + auto write(QByteArray& data) -> Task::State override; + auto abort() -> Task::State override; + auto finalize(QNetworkReply& reply) -> Task::State override; + auto hasLocalData() -> bool override { return false; } + + private: + const PasteType m_paste_type; + const QString m_base_url; + ResultPtr m_result; + QByteArray m_output; + }; + PasteUpload(const QString& log, PasteType pasteType); + virtual ~PasteUpload() = default; + + static NetRequest::Ptr make(const QString& log, PasteType pasteType, QString baseURL, ResultPtr result); private: - QWidget* m_window; - QString m_pasteLink; - QString m_baseUrl; - QString m_uploadUrl; - PasteType m_pasteType; - QByteArray m_text; - std::shared_ptr m_reply; - public slots: - void downloadError(QNetworkReply::NetworkError); - void downloadFinished(); -}; + virtual QNetworkReply* getReply(QNetworkRequest&) override; + QString m_log; + const PasteType m_paste_type; +}; \ No newline at end of file diff --git a/launcher/ui/GuiUtil.cpp b/launcher/ui/GuiUtil.cpp index d53ade86d..adb6e8bf2 100644 --- a/launcher/ui/GuiUtil.cpp +++ b/launcher/ui/GuiUtil.cpp @@ -38,10 +38,16 @@ #include "GuiUtil.h" #include +#include #include #include #include +#include + +#include "FileSystem.h" +#include "logs/AnonymizeLog.h" +#include "net/NetJob.h" #include "net/PasteUpload.h" #include "ui/dialogs/CustomMessageBox.h" #include "ui/dialogs/ProgressDialog.h" @@ -74,52 +80,52 @@ QString truncateLogForMclogs(const QString& logContent) return logContent; } +std::optional GuiUtil::uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget) +{ + return uploadPaste(name, FS::read(filePath.absoluteFilePath()), parentWidget); +}; + std::optional GuiUtil::uploadPaste(const QString& name, const QString& text, QWidget* parentWidget) { ProgressDialog dialog(parentWidget); - auto pasteTypeSetting = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); - auto pasteCustomAPIBaseSetting = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); + auto pasteType = static_cast(APPLICATION->settings()->get("PastebinType").toInt()); + auto baseURL = APPLICATION->settings()->get("PastebinCustomAPIBase").toString(); bool shouldTruncate = false; - { - QUrl baseUrl; - if (pasteCustomAPIBaseSetting.isEmpty()) - baseUrl = PasteUpload::PasteTypes[pasteTypeSetting].defaultBase; - else - baseUrl = pasteCustomAPIBaseSetting; + if (baseURL.isEmpty()) + baseURL = PasteUpload::PasteTypes[pasteType].defaultBase; - if (baseUrl.isValid()) { - auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), - QObject::tr("You are about to upload \"%1\" to %2.\n" - "You should double-check for personal information.\n\n" - "Are you sure?") - .arg(name, baseUrl.host()), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) - ->exec(); + if (auto url = QUrl(baseURL); url.isValid()) { + auto response = CustomMessageBox::selectable(parentWidget, QObject::tr("Confirm Upload"), + QObject::tr("You are about to upload \"%1\" to %2.\n" + "You should double-check for personal information.\n\n" + "Are you sure?") + .arg(name, url.host()), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No, QMessageBox::No) + ->exec(); - if (response != QMessageBox::Yes) + if (response != QMessageBox::Yes) + return {}; + + if (baseURL == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { + auto truncateResponse = CustomMessageBox::selectable( + parentWidget, QObject::tr("Confirm Truncation"), + QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" + "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" + "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " + "potentially useful info like crashes at the end.\n\n" + "Proceed with truncation?") + .arg(text.count("\n")) + .arg(MaxMclogsLines) + .arg(InitialMclogsLines) + .arg(FinalMclogsLines), + QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) + ->exec(); + + if (truncateResponse == QMessageBox::Cancel) { return {}; - - if (baseUrl.toString() == "https://api.mclo.gs" && text.count("\n") > MaxMclogsLines) { - auto truncateResponse = CustomMessageBox::selectable( - parentWidget, QObject::tr("Confirm Truncation"), - QObject::tr("The log has %1 lines, exceeding mclo.gs' limit of %2.\n" - "The launcher can keep the first %3 and last %4 lines, trimming the middle.\n\n" - "If you choose 'No', mclo.gs will only keep the first %2 lines, cutting off " - "potentially useful info like crashes at the end.\n\n" - "Proceed with truncation?") - .arg(text.count("\n")) - .arg(MaxMclogsLines) - .arg(InitialMclogsLines) - .arg(FinalMclogsLines), - QMessageBox::Warning, QMessageBox::Yes | QMessageBox::No | QMessageBox::Cancel, QMessageBox::No) - ->exec(); - - if (truncateResponse == QMessageBox::Cancel) { - return {}; - } - shouldTruncate = truncateResponse == QMessageBox::Yes; } + shouldTruncate = truncateResponse == QMessageBox::Yes; } } @@ -128,26 +134,48 @@ std::optional GuiUtil::uploadPaste(const QString& name, const QString& textToUpload = truncateLogForMclogs(text); } - std::unique_ptr paste(new PasteUpload(parentWidget, textToUpload, pasteCustomAPIBaseSetting, pasteTypeSetting)); + auto result = std::make_shared(); + auto job = NetJob::Ptr(new NetJob("Log Upload", APPLICATION->network())); - dialog.execWithTask(paste.get()); - if (!paste->wasSuccessful()) { - CustomMessageBox::selectable(parentWidget, QObject::tr("Upload failed"), paste->failReason(), QMessageBox::Critical)->exec(); - return QString(); - } else { - const QString link = paste->pasteLink(); - setClipboardText(link); + job->addNetAction(PasteUpload::make(textToUpload, pasteType, baseURL, result)); + QObject::connect(job.get(), &Task::failed, [parentWidget](QString reason) { + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), reason, QMessageBox::Critical)->show(); + }); + QObject::connect(job.get(), &Task::aborted, [parentWidget] { + CustomMessageBox::selectable(parentWidget, QObject::tr("Logs upload aborted"), + QObject::tr("The task has been aborted by the user."), QMessageBox::Information) + ->show(); + }); + + if (dialog.execWithTask(job.get()) == QDialog::Accepted) { + if (!result->error.isEmpty() || !result->extra_message.isEmpty()) { + QString message = QObject::tr("Error: %1").arg(result->error); + if (!result->extra_message.isEmpty()) { + message += QObject::tr("\nError message: %1").arg(result->extra_message); + } + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), message, QMessageBox::Critical)->show(); + return {}; + } + if (result->link.isEmpty()) { + CustomMessageBox::selectable(parentWidget, QObject::tr("Failed to upload logs!"), "The upload link is empty", + QMessageBox::Critical) + ->show(); + return {}; + } + setClipboardText(result->link); CustomMessageBox::selectable( parentWidget, QObject::tr("Upload finished"), - QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(link), + QObject::tr("The link to the uploaded log has been placed in your clipboard.").arg(result->link), QMessageBox::Information) ->exec(); - return link; + return result->link; } + return {}; } -void GuiUtil::setClipboardText(const QString& text) +void GuiUtil::setClipboardText(QString text) { + anonymizeLog(text); QApplication::clipboard()->setText(text); } diff --git a/launcher/ui/GuiUtil.h b/launcher/ui/GuiUtil.h index 8d384d3f6..c3ba01f5b 100644 --- a/launcher/ui/GuiUtil.h +++ b/launcher/ui/GuiUtil.h @@ -1,11 +1,13 @@ #pragma once +#include #include #include namespace GuiUtil { -std::optional uploadPaste(const QString& name, const QString& text, QWidget* parentWidget); -void setClipboardText(const QString& text); +std::optional uploadPaste(const QString& name, const QFileInfo& filePath, QWidget* parentWidget); +std::optional uploadPaste(const QString& name, const QString& data, QWidget* parentWidget); +void setClipboardText(QString text); QStringList BrowseForFiles(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); QString BrowseForFile(QString context, QString caption, QString filter, QString defaultPath, QWidget* parentWidget); } // namespace GuiUtil diff --git a/launcher/ui/MainWindow.cpp b/launcher/ui/MainWindow.cpp index e30c50782..232ba45cd 100644 --- a/launcher/ui/MainWindow.cpp +++ b/launcher/ui/MainWindow.cpp @@ -90,6 +90,7 @@ #include #include "InstanceWindow.h" +#include "ui/GuiUtil.h" #include "ui/dialogs/AboutDialog.h" #include "ui/dialogs/CopyInstanceDialog.h" #include "ui/dialogs/CustomMessageBox.h" @@ -255,6 +256,16 @@ MainWindow::MainWindow(QWidget* parent) : QMainWindow(parent), ui(new Ui::MainWi ui->actionViewJavaFolder->setEnabled(BuildConfig.JAVA_DOWNLOADER_ENABLED); } + { // logs upload + + auto menu = new QMenu(this); + for (auto file : QDir("logs").entryInfoList(QDir::Files)) { + auto action = menu->addAction(file.fileName()); + connect(action, &QAction::triggered, this, [this, file] { GuiUtil::uploadPaste(file.fileName(), file, this); }); + } + ui->actionUploadLog->setMenu(menu); + } + // add the toolbar toggles to the view menu ui->viewMenu->addAction(ui->instanceToolBar->toggleViewAction()); ui->viewMenu->addAction(ui->newsToolBar->toggleViewAction()); diff --git a/launcher/ui/MainWindow.ui b/launcher/ui/MainWindow.ui index 3fa30c97e..69d31589b 100644 --- a/launcher/ui/MainWindow.ui +++ b/launcher/ui/MainWindow.ui @@ -215,6 +215,7 @@ + @@ -662,6 +663,18 @@ Clear cached metadata + + + + .. + + + Upload logs + + + Upload launcher logs to the selected log provider + +