Files
modorganizer/src/nexusinterface.cpp
T
isanae 4c5e3da233 moved nexus api stuff to GlobalSettings
pass a pointer to Settings around for things that can be called without settings, when creating the first instance
added dummy plugin list, mod list and iorganizer to initialize plugins without an instance
moved PluginContainer into the core filter, had nothing to do with the plugins list
NexusInterface is now created manually instead of being a static singleton because it needs to know if the settings are available
2020-11-03 11:39:08 -05:00

1176 lines
41 KiB
C++

/*
Copyright (C) 2012 Sebastian Herbord. All rights reserved.
This file is part of Mod Organizer.
Mod Organizer 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, either version 3 of the License, or
(at your option) any later version.
Mod Organizer 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 Mod Organizer. If not, see <http://www.gnu.org/licenses/>.
*/
#include "nexusinterface.h"
#include "iplugingame.h"
#include "nxmaccessmanager.h"
#include "selectiondialog.h"
#include "bbcode.h"
#include "settings.h"
#include <utility.h>
#include "shared/util.h"
#include <log.h>
#include <moassert.h>
#include <QApplication>
#include <QNetworkCookieJar>
#include <QJsonDocument>
#include <QRegularExpression>
#include <regex>
using namespace MOBase;
using namespace MOShared;
void throttledWarning(const APIUserAccount& user)
{
log::error(
"You have fewer than {} requests remaining ({}). Only downloads and "
"login validation are being allowed.",
APIUserAccount::ThrottleThreshold,
user.remainingRequests());
}
NexusBridge::NexusBridge(PluginContainer *pluginContainer, const QString &subModule)
: m_Interface(&NexusInterface::instance())
, m_SubModule(subModule)
{
}
void NexusBridge::requestDescription(QString gameName, int modID, QVariant userData)
{
m_RequestIDs.insert(m_Interface->requestDescription(gameName, modID, this, userData, m_SubModule));
}
void NexusBridge::requestFiles(QString gameName, int modID, QVariant userData)
{
m_RequestIDs.insert(m_Interface->requestFiles(gameName, modID, this, userData, m_SubModule));
}
void NexusBridge::requestFileInfo(QString gameName, int modID, int fileID, QVariant userData)
{
m_RequestIDs.insert(m_Interface->requestFileInfo(gameName, modID, fileID, this, userData, m_SubModule));
}
void NexusBridge::requestDownloadURL(QString gameName, int modID, int fileID, QVariant userData)
{
m_RequestIDs.insert(m_Interface->requestDownloadURL(gameName, modID, fileID, this, userData, m_SubModule));
}
void NexusBridge::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QVariant userData)
{
m_RequestIDs.insert(m_Interface->requestToggleEndorsement(gameName, modID, modVersion, endorse, this, userData, m_SubModule));
}
void NexusBridge::requestToggleTracking(QString gameName, int modID, bool track, QVariant userData)
{
m_RequestIDs.insert(m_Interface->requestToggleTracking(gameName, modID, track, this, userData, m_SubModule));
}
void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit descriptionAvailable(gameName, modID, userData, resultData);
}
}
void NexusBridge::nxmFilesAvailable(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
QList<ModRepositoryFileInfo> fileInfoList;
QVariantMap resultInfo = resultData.toMap();
QList resultList = resultInfo["files"].toList();
for (const QVariant &file : resultList) {
ModRepositoryFileInfo temp;
QVariantMap fileInfo = file.toMap();
temp.uri = fileInfo["file_name"].toString();
temp.name = fileInfo["name"].toString();
temp.description = BBCode::convertToHTML(fileInfo["description"].toString());
temp.version = VersionInfo(fileInfo["version"].toString());
temp.categoryID = fileInfo["category_id"].toInt();
temp.fileID = fileInfo["file_id"].toInt();
temp.fileSize = fileInfo["size"].toInt();
fileInfoList.append(temp);
}
emit filesAvailable(gameName, modID, userData, fileInfoList);
}
}
void NexusBridge::nxmFileInfoAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit fileInfoAvailable(gameName, modID, fileID, userData, resultData);
}
}
void NexusBridge::nxmDownloadURLsAvailable(QString gameName, int modID, int fileID, QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit downloadURLsAvailable(gameName, modID, fileID, userData, resultData);
}
}
void NexusBridge::nxmEndorsementsAvailable(QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit endorsementsAvailable(userData, resultData);
}
}
void NexusBridge::nxmEndorsementToggled(QString gameName, int modID, QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit endorsementToggled(gameName, modID, userData, resultData);
}
}
void NexusBridge::nxmTrackedModsAvailable(QVariant userData, QVariant resultData, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit trackedModsAvailable(userData, resultData);
}
}
void NexusBridge::nxmTrackingToggled(QString gameName, int modID, QVariant userData, bool tracked, int requestID)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit trackingToggled(gameName, modID, userData, tracked);
}
}
void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID, QVariant userData, int requestID, QNetworkReply::NetworkError error, const QString &errorMessage)
{
std::set<int>::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit requestFailed(gameName, modID, fileID, userData, error, errorMessage);
}
}
QAtomicInt NexusInterface::NXMRequestInfo::s_NextID(0);
APILimits NexusInterface::defaultAPILimits()
{
// https://app.swaggerhub.com/apis-docs/NexusMods/nexus-mods_public_api_params_in_form_data/1.0#/
const int MaxDaily = 2500;
const int MaxHourly = 100;
APILimits limits;
limits.maxDailyRequests = MaxDaily;
limits.remainingDailyRequests = MaxDaily;
limits.maxHourlyRequests = MaxHourly;
limits.remainingHourlyRequests = MaxHourly;
return limits;
}
APILimits NexusInterface::parseLimits(const QNetworkReply* reply)
{
return parseLimits(reply->rawHeaderPairs());
}
APILimits NexusInterface::parseLimits(
const QList<QNetworkReply::RawHeaderPair>& headers)
{
APILimits limits;
for (const auto& pair : headers) {
const auto name = QString(pair.first).toLower();
if (name == "x-rl-daily-limit") {
limits.maxDailyRequests = pair.second.toInt();
} else if (name == "x-rl-daily-remaining") {
limits.remainingDailyRequests = pair.second.toInt();
} else if (name == "x-rl-hourly-limit") {
limits.maxHourlyRequests = pair.second.toInt();
} else if (name == "x-rl-hourly-remaining") {
limits.remainingHourlyRequests = pair.second.toInt();
}
}
return limits;
}
static NexusInterface* g_instance = nullptr;
NexusInterface::NexusInterface(Settings* s)
: m_PluginContainer(nullptr)
{
MO_ASSERT(!g_instance);
g_instance = this;
m_User.limits(defaultAPILimits());
m_MOVersion = createVersionInfo();
m_AccessManager = new NXMAccessManager(
this, s, m_MOVersion.displayString(3));
m_DiskCache = new QNetworkDiskCache(this);
connect(m_AccessManager, SIGNAL(requestNXMDownload(QString)), this, SLOT(downloadRequestedNXM(QString)));
}
NexusInterface::~NexusInterface()
{
cleanup();
MO_ASSERT(g_instance == this);
g_instance = nullptr;
}
NXMAccessManager *NexusInterface::getAccessManager()
{
return m_AccessManager;
}
NexusInterface& NexusInterface::instance()
{
MO_ASSERT(g_instance);
return *g_instance;
}
void NexusInterface::setCacheDirectory(const QString &directory)
{
m_DiskCache->setCacheDirectory(directory);
m_AccessManager->setCache(m_DiskCache);
}
void NexusInterface::loginCompleted()
{
nextRequest();
}
void NexusInterface::setUserAccount(const APIUserAccount& user)
{
m_User = user;
emit requestsChanged(getAPIStats(), m_User);
}
void NexusInterface::interpretNexusFileName(const QString &fileName, QString &modName, int &modID, bool query)
{
// guess the mod name from the file name
static const QRegularExpression complex(R"(^([a-zA-Z0-9_'"\-.() ]*?)([-_ ][VvRr]+[0-9]+(?:(?:[\.][0-9]+){0,2}|(?:[_][0-9]+){0,2}|(?:[-.][0-9]+){0,2})?[ab]?)??-([1-9][0-9]+)?-.*?\.(zip|rar|7z))");
//complex regex explanation:
//group 1: modname.
//group 2: optional version,
// assumed to start with v (empty most of the time).
//group 3: NexusId,
// assumed wrapped in "-", will miss single digit IDs for better accuracy.
//If no id is present the whole regex does not match.
static const QRegularExpression simple(R"(^[^a-zA-Z]*([a-zA-Z_ ]+))");
auto complexMatch = complex.match(fileName);
auto simpleMatch = simple.match(fileName);
// assume not found
modID = -1;
if (complexMatch.hasMatch()) {
modName = complexMatch.captured(1);
if (!query && !complexMatch.captured(3).isNull()) {
modID = complexMatch.captured(3).toInt();
}
}
else if (simpleMatch.hasMatch()) {
modName = simpleMatch.captured(0);
}
else {
modName.clear();
}
if (query) {
SelectionDialog selection(tr("Please pick the mod ID for \"%1\"").arg(fileName));
int index = 0;
auto splits = fileName.split(QRegExp("[^0-9]"), QString::KeepEmptyParts);
for (auto substr : splits) {
bool ok = false;
int value = substr.toInt(&ok);
if (ok) {
QString highlight(fileName);
highlight.insert(index, " *");
highlight.insert(index + substr.length() + 2, "* ");
QStringList choice;
choice << substr;
choice << (index > 0 ? fileName.left(index - 1) : substr);
selection.addChoice(substr, highlight, choice);
}
index += substr.length() + 1;
}
if (selection.numChoices() > 0) {
if (selection.exec() == QDialog::Accepted) {
auto choice = selection.getChoiceData().toStringList();
modID = choice.at(0).toInt();
modName = choice.at(1);
modName = modName.replace('_', ' ').trimmed();
log::debug("user selected mod ID {} and mod name \"{}\"", modID, modName);
}
else {
log::debug("user canceled mod ID selection");
}
}
else {
log::debug("no possible mod IDs found in file name");
}
}
}
bool NexusInterface::isURLGameRelated(const QUrl &url) const
{
QString const name(url.toString());
return name.startsWith(getGameURL("") + "/") ||
name.startsWith(getOldModsURL("") + "/");
}
QString NexusInterface::getGameURL(QString gameName) const
{
IPluginGame *game = getGame(gameName);
if (game != nullptr) {
return "https://www.nexusmods.com/" + game->gameNexusName().toLower();
} else {
log::error("getGameURL can't find plugin for {}", gameName);
return "";
}
}
QString NexusInterface::getOldModsURL(QString gameName) const
{
IPluginGame *game = getGame(gameName);
if (game != nullptr) {
return "https://" + game->gameNexusName().toLower() + ".nexusmods.com/mods";
} else {
log::error("getOldModsURL can't find plugin for {}", gameName);
return "";
}
}
QString NexusInterface::getModURL(int modID, QString gameName = "") const
{
return QString("%1/mods/%2").arg(getGameURL(gameName)).arg(modID);
}
std::vector<std::pair<QString, QString>> NexusInterface::getGameChoices(const MOBase::IPluginGame *game)
{
std::vector<std::pair<QString, QString>> choices;
choices.push_back(std::pair<QString, QString>(game->gameShortName(), game->gameName()));
for (QString gameName : game->validShortNames()) {
for (auto gamePlugin : m_PluginContainer->plugins<IPluginGame>()) {
if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
choices.push_back(std::pair<QString, QString>(gamePlugin->gameShortName(), gamePlugin->gameName()));
break;
}
}
}
return choices;
}
bool NexusInterface::isModURL(int modID, const QString &url) const
{
if (QUrl(url) == QUrl(getModURL(modID))) {
return true;
}
//Try the alternate (old style) mod name
QString alt = QString("%1/%2").arg(getOldModsURL("")).arg(modID);
return QUrl(alt) == QUrl(url);
}
void NexusInterface::setPluginContainer(PluginContainer *pluginContainer)
{
m_PluginContainer = pluginContainer;
}
int NexusInterface::requestDescription(QString gameName, int modID, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData, subModule, game);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)),
receiver, SLOT(nxmDescriptionAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestModInfo(QString gameName, int modID, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule, game);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)),
receiver, SLOT(nxmModInfoAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestUpdateInfo(QString gameName, NexusInterface::UpdatePeriod period, QObject *receiver, QVariant userData,
const QString &subModule, const MOBase::IPluginGame *game)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData, subModule, game);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)),
receiver, SLOT(nxmUpdateInfoAvailable(QString, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestUpdates(const int &modID, QObject *receiver, QVariant userData,
QString gameName, const QString &subModule)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
IPluginGame *game = getGame(gameName);
if (game == nullptr) {
log::error("requestUpdates can't find plugin for {}", gameName);
return -1;
}
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_GETUPDATES, userData, subModule, game);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)),
receiver, SLOT(nxmUpdatesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
void NexusInterface::fakeFiles()
{
static int id = 42;
QVariantList result;
QVariantMap fileMap;
fileMap["uri"] = "fakeURI";
fileMap["name"] = "fakeName";
fileMap["description"] = "fakeDescription";
fileMap["version"] = "1.0.0";
fileMap["category_id"] = "1";
fileMap["id"] = "1";
fileMap["size"] = "512";
result.append(fileMap);
emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++);
}
int NexusInterface::requestFiles(QString gameName, int modID, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule, game);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmFilesAvailable(QString, int, QVariant, QVariant, int)),
receiver, SLOT(nxmFilesAvailable(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData, const QString &subModule)
{
IPluginGame *gamePlugin = getGame(gameName);
if (gamePlugin == nullptr) {
log::error("requestFileInfo can't find plugin for {}", gameName);
return -1;
}
NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData, subModule, gamePlugin);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)),
receiver, SLOT(nxmFileInfoAvailable(QString, int, int, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData, subModule, game);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)),
receiver, SLOT(nxmDownloadURLsAvailable(QString,int,int,QVariant,QVariant,int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString,int,int,QVariant,int,QNetworkReply::NetworkError,QString)),
receiver, SLOT(nxmRequestFailed(QString,int,int,QVariant,int,QNetworkReply::NetworkError,QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestEndorsementInfo(QObject *receiver, QVariant userData, const QString &subModule)
{
NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_ENDORSEMENTS, userData, subModule);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmEndorsementsAvailable(QVariant, QVariant, int)),
receiver, SLOT(nxmEndorsementsAvailable(QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestToggleEndorsement(QString gameName, int modID, QString modVersion, bool endorse, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT, userData, subModule, game);
requestInfo.m_Endorse = endorse;
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)),
receiver, SLOT(nxmEndorsementToggled(QString, int, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestTrackingInfo(QObject *receiver, QVariant userData, const QString &subModule)
{
NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_TRACKEDMODS, userData, subModule);
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmTrackedModsAvailable(QVariant, QVariant, int)),
receiver, SLOT(nxmTrackedModsAvailable(QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestToggleTracking(QString gameName, int modID, bool track, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLETRACKING, userData, subModule, game);
requestInfo.m_Track = track;
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmTrackingToggled(QString, int, QVariant, bool, int)),
receiver, SLOT(nxmTrackingToggled(QString, int, QVariant, bool, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray &hash, QObject *receiver, QVariant userData,
const QString &subModule, MOBase::IPluginGame const *game)
{
NXMRequestInfo requestInfo(hash, NXMRequestInfo::TYPE_FILEINFO_MD5, userData, subModule, game);
requestInfo.m_Hash = hash;
requestInfo.m_AllowedErrors[QNetworkReply::NetworkError::ContentNotFoundError].append(404);
requestInfo.m_IgnoreGenericErrorHandler = true;
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)),
receiver, SLOT(nxmFileInfoFromMd5Available(QString, QVariant, QVariant, int)), Qt::UniqueConnection);
connect(this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, QNetworkReply::NetworkError, QString)), Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
IPluginGame* NexusInterface::getGame(QString gameName) const
{
auto gamePlugins = m_PluginContainer->plugins<IPluginGame>();
IPluginGame *gamePlugin = qApp->property("managed_game").value<IPluginGame*>();
for (auto plugin : gamePlugins) {
if (plugin != nullptr && plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
gamePlugin = plugin;
break;
}
}
return gamePlugin;
}
void NexusInterface::cleanup()
{
m_AccessManager = nullptr;
m_DiskCache = nullptr;
}
void NexusInterface::clearCache()
{
m_DiskCache->clear();
m_AccessManager->clearCookies();
}
void NexusInterface::nextRequest()
{
if ((m_ActiveRequest.size() >= MAX_ACTIVE_DOWNLOADS)
|| m_RequestQueue.isEmpty()) {
return;
}
if (!getAccessManager()->validated()) {
if (!getAccessManager()->validateAttempted()) {
emit needLogin();
return;
} else if (getAccessManager()->validateWaiting()) {
return;
} else {
log::error("{}", tr("You must authorize MO2 in Settings -> Nexus to use the Nexus API."));
}
}
if (m_User.exhausted()) {
m_RequestQueue.clear();
QTime time = QTime::currentTime();
QTime targetTime;
targetTime.setHMS((time.hour() + 1) % 23, 5, 0);
QString warning = tr(
"You've exceeded the Nexus API rate limit and requests are now being throttled. "
"Your next batch of requests will be available in approximately %1 minutes and %2 seconds.")
.arg(time.secsTo(targetTime) / 60)
.arg(time.secsTo(targetTime) % 60);
log::warn("{}", warning);
return;
}
NXMRequestInfo info = m_RequestQueue.dequeue();
info.m_Timeout = new QTimer(this);
info.m_Timeout->setInterval(60000);
QJsonObject postObject;
QJsonDocument postData(postObject);
bool requestIsDelete = false;
QString url;
if (!info.m_Reroute) {
bool hasParams = false;
switch (info.m_Type) {
case NXMRequestInfo::TYPE_DESCRIPTION:
case NXMRequestInfo::TYPE_MODINFO: {
url = QString("%1/games/%2/mods/%3").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID);
} break;
case NXMRequestInfo::TYPE_CHECKUPDATES: {
QString period;
switch (info.m_UpdatePeriod) {
case UpdatePeriod::DAY:
period = "1d";
break;
case UpdatePeriod::WEEK:
period = "1w";
break;
case UpdatePeriod::MONTH:
period = "1m";
break;
}
url = QString("%1/games/%2/mods/updated?period=%3").arg(info.m_URL).arg(info.m_GameName).arg(period);
} break;
case NXMRequestInfo::TYPE_FILES:
case NXMRequestInfo::TYPE_GETUPDATES: {
url = QString("%1/games/%2/mods/%3/files").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID);
} break;
case NXMRequestInfo::TYPE_FILEINFO: {
url = QString("%1/games/%2/mods/%3/files/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID);
} break;
case NXMRequestInfo::TYPE_DOWNLOADURL: {
ModRepositoryFileInfo *fileInfo = qobject_cast<ModRepositoryFileInfo*>(qvariant_cast<QObject*>(info.m_UserData));
if (m_User.type() == APIUserAccountTypes::Premium) {
url = QString("%1/games/%2/mods/%3/files/%4/download_link").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID);
} else if (!fileInfo->nexusKey.isEmpty() && fileInfo->nexusExpires && fileInfo->nexusDownloadUser == m_User.id().toInt()) {
url = QString("%1/games/%2/mods/%3/files/%4/download_link?key=%5&expires=%6")
.arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(info.m_FileID).arg(fileInfo->nexusKey).arg(fileInfo->nexusExpires);
} else {
log::warn("{}", tr("Aborting download: Either you clicked on a premium-only link and your account is not premium, "
"or the download link was generated by a different account than the one stored in Mod Organizer."));
return;
}
} break;
case NXMRequestInfo::TYPE_ENDORSEMENTS: {
url = QString("%1/user/endorsements").arg(info.m_URL);
} break;
case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: {
QString endorse = info.m_Endorse ? "endorse" : "abstain";
url = QString("%1/games/%2/mods/%3/%4").arg(info.m_URL).arg(info.m_GameName).arg(info.m_ModID).arg(endorse);
postObject.insert("Version", info.m_ModVersion);
postData.setObject(postObject);
} break;
case NXMRequestInfo::TYPE_TOGGLETRACKING: {
url = QStringLiteral("%1/user/tracked_mods?domain_name=%2").arg(info.m_URL).arg(info.m_GameName);
postObject.insert("mod_id", info.m_ModID);
postData.setObject(postObject);
requestIsDelete = !info.m_Track;
} break;
case NXMRequestInfo::TYPE_TRACKEDMODS: {
url = QStringLiteral("%1/user/tracked_mods").arg(info.m_URL);
} break;
case NXMRequestInfo::TYPE_FILEINFO_MD5: {
url = QStringLiteral("%1/games/%2/mods/md5_search/%3").arg(info.m_URL).arg(info.m_GameName).arg(QString(info.m_Hash.toHex()));
}
}
} else {
url = info.m_URL;
}
QNetworkRequest request(url);
request.setAttribute(QNetworkRequest::CacheSaveControlAttribute, false);
request.setAttribute(QNetworkRequest::CacheLoadControlAttribute, QNetworkRequest::AlwaysNetwork);
request.setRawHeader("APIKEY", m_User.apiKey().toUtf8());
request.setHeader(QNetworkRequest::KnownHeaders::UserAgentHeader, m_AccessManager->userAgent(info.m_SubModule));
request.setHeader(QNetworkRequest::KnownHeaders::ContentTypeHeader, "application/json");
request.setRawHeader("Protocol-Version", "1.0.0");
request.setRawHeader("Application-Name", "MO2");
request.setRawHeader("Application-Version", QApplication::applicationVersion().toUtf8());
if (postData.object().isEmpty()) {
if (!requestIsDelete) {
info.m_Reply = m_AccessManager->get(request);
} else {
info.m_Reply = m_AccessManager->deleteResource(request);
}
} else if (!requestIsDelete) {
info.m_Reply = m_AccessManager->post(request, postData.toJson());
} else {
// Qt doesn't support DELETE with a payload as that's technically against the HTTP standard...
info.m_Reply = m_AccessManager->sendCustomRequest(request, "DELETE", postData.toJson());
}
connect(info.m_Reply, SIGNAL(finished()), this, SLOT(requestFinished()));
if (!info.m_IgnoreGenericErrorHandler)
connect(info.m_Reply, SIGNAL(error(QNetworkReply::NetworkError)), this, SLOT(requestError(QNetworkReply::NetworkError)));
connect(info.m_Timeout, SIGNAL(timeout()), this, SLOT(requestTimeout()));
info.m_Timeout->start();
m_ActiveRequest.push_back(info);
}
void NexusInterface::downloadRequestedNXM(const QString &url)
{
emit requestNXMDownload(url);
}
void NexusInterface::requestFinished(std::list<NXMRequestInfo>::iterator iter)
{
QNetworkReply *reply = iter->m_Reply;
auto error = reply->error();
if (error != QNetworkReply::NoError) {
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (iter->m_AllowedErrors.contains(error) && iter->m_AllowedErrors[error].contains(statusCode)) {
// These errors are allows to silently happen. They should be handled in nxmRequestFailed below.
} else if (statusCode == 429) {
m_User.limits(parseLimits(reply));
if (!m_User.exhausted()) {
log::warn("You appear to be making requests to the Nexus API too quickly and are being throttled. Please inform the MO2 team.");
}
else {
log::warn("All API requests have been consumed and are now being denied.");
}
emit requestsChanged(getAPIStats(), m_User);
log::warn("Error: {}", reply->errorString());
} else {
log::warn("request failed: {}", reply->errorString());
}
emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), reply->errorString());
} else {
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
if (statusCode == 301) {
// redirect request, return request to queue
iter->m_URL = reply->attribute(QNetworkRequest::RedirectionTargetAttribute).toString();
iter->m_Reroute = true;
m_RequestQueue.enqueue(*iter);
//nextRequest();
return;
}
QByteArray data = reply->readAll();
if (data.isNull() || data.isEmpty() || (strcmp(data.constData(), "null") == 0)) {
QString nexusError(reply->rawHeader("NexusErrorInfo"));
if (nexusError.length() == 0) {
nexusError = tr("empty response");
}
log::debug("nexus error: {}", nexusError);
emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), nexusError);
} else {
QJsonDocument responseDoc = QJsonDocument::fromJson(data);
if (!responseDoc.isNull()) {
QVariant result = responseDoc.toVariant();
switch (iter->m_Type) {
case NXMRequestInfo::TYPE_DESCRIPTION: {
emit nxmDescriptionAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_MODINFO: {
emit nxmModInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_CHECKUPDATES: {
emit nxmUpdateInfoAvailable(iter->m_GameName, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_FILES: {
emit nxmFilesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_GETUPDATES: {
emit nxmUpdatesAvailable(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_FILEINFO: {
emit nxmFileInfoAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_DOWNLOADURL: {
emit nxmDownloadURLsAvailable(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_ENDORSEMENTS: {
emit nxmEndorsementsAvailable(iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_TOGGLEENDORSEMENT: {
emit nxmEndorsementToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_TOGGLETRACKING: {
auto results = result.toMap();
auto message = results["message"].toString();
if (message.contains(QRegularExpression("User [0-9]+ is already Tracking Mod: [0-9]+")) ||
message.contains(QRegularExpression("User [0-9]+ is now Tracking Mod: [0-9]+"))) {
emit nxmTrackingToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, true, iter->m_ID);
} else if (message.contains(QRegularExpression("User [0-9]+ is no longer tracking [0-9]+")) ||
message.contains(QRegularExpression("Users is not tracking mod. Unable to untrack."))) {
emit nxmTrackingToggled(iter->m_GameName, iter->m_ModID, iter->m_UserData, false, iter->m_ID);
}
} break;
case NXMRequestInfo::TYPE_TRACKEDMODS: {
emit nxmTrackedModsAvailable(iter->m_UserData, result, iter->m_ID);
} break;
case NXMRequestInfo::TYPE_FILEINFO_MD5: {
emit nxmFileInfoFromMd5Available(iter->m_GameName, iter->m_UserData, result, iter->m_ID);
} break;
}
m_User.limits(parseLimits(reply));
emit requestsChanged(getAPIStats(), m_User);
} else {
emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID, iter->m_UserData, iter->m_ID, reply->error(), tr("invalid response"));
}
}
}
}
void NexusInterface::requestFinished()
{
QNetworkReply *reply = static_cast<QNetworkReply*>(sender());
for (std::list<NXMRequestInfo>::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) {
if (iter->m_Reply == reply) {
iter->m_Timeout->stop();
iter->m_Timeout->deleteLater();
requestFinished(iter);
iter->m_Reply->deleteLater();
m_ActiveRequest.erase(iter);
nextRequest();
return;
}
}
}
void NexusInterface::requestError(QNetworkReply::NetworkError)
{
QNetworkReply *reply = qobject_cast<QNetworkReply*>(sender());
if (reply == nullptr) {
log::warn("invalid sender type");
return;
}
log::error(
"request ({}) error: {} ({})",
reply->url().toString(), reply->errorString(), reply->error());
}
void NexusInterface::requestTimeout()
{
QTimer *timer = qobject_cast<QTimer*>(sender());
if (timer == nullptr) {
log::warn("invalid sender type");
return;
}
for (std::list<NXMRequestInfo>::iterator iter = m_ActiveRequest.begin(); iter != m_ActiveRequest.end(); ++iter) {
if (iter->m_Timeout == timer) {
// this abort causes a "request failed" which cleans up the rest
iter->m_Reply->abort();
return;
}
}
}
APIUserAccount NexusInterface::getAPIUserAccount() const
{
return m_User;
}
APIStats NexusInterface::getAPIStats() const
{
APIStats stats;
stats.requestsQueued = m_RequestQueue.size();
return stats;
}
namespace {
QString get_management_url()
{
return "https://api.nexusmods.com/v1";
}
}
NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
, NexusInterface::NXMRequestInfo::Type type
, QVariant userData
, const QString &subModule
, MOBase::IPluginGame const *game
)
: m_ModID(modID)
, m_ModVersion("0")
, m_FileID(0)
, m_Reply(nullptr)
, m_Type(type)
, m_UpdatePeriod(UpdatePeriod::NONE)
, m_UserData(userData)
, m_Timeout(nullptr)
, m_Reroute(false)
, m_ID(s_NextID.fetchAndAddAcquire(1))
, m_URL(get_management_url())
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
, m_GameName(game->gameNexusName())
, m_Endorse(false)
, m_Track(false)
, m_Hash(QByteArray())
{
}
NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
, QString modVersion
, NexusInterface::NXMRequestInfo::Type type
, QVariant userData
, const QString &subModule
, MOBase::IPluginGame const *game
)
: m_ModID(modID)
, m_ModVersion(modVersion)
, m_FileID(0)
, m_Reply(nullptr)
, m_Type(type)
, m_UpdatePeriod(UpdatePeriod::NONE)
, m_UserData(userData)
, m_Timeout(nullptr)
, m_Reroute(false)
, m_ID(s_NextID.fetchAndAddAcquire(1))
, m_URL(get_management_url())
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
, m_GameName(game->gameNexusName())
, m_Endorse(false)
, m_Track(false)
, m_Hash(QByteArray())
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(int modID
, int fileID
, NexusInterface::NXMRequestInfo::Type type
, QVariant userData
, const QString &subModule
, MOBase::IPluginGame const *game
)
: m_ModID(modID)
, m_ModVersion("0")
, m_FileID(fileID)
, m_Reply(nullptr)
, m_Type(type)
, m_UpdatePeriod(UpdatePeriod::NONE)
, m_UserData(userData)
, m_Timeout(nullptr)
, m_Reroute(false)
, m_ID(s_NextID.fetchAndAddAcquire(1))
, m_URL(get_management_url())
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
, m_GameName(game->gameNexusName())
, m_Endorse(false)
, m_Track(false)
, m_Hash(QByteArray())
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type
, QVariant userData
, const QString &subModule
)
: m_ModID(0)
, m_ModVersion("0")
, m_FileID(0)
, m_Reply(nullptr)
, m_Type(type)
, m_UpdatePeriod(UpdatePeriod::NONE)
, m_UserData(userData)
, m_Timeout(nullptr)
, m_Reroute(false)
, m_ID(s_NextID.fetchAndAddAcquire(1))
, m_URL(get_management_url())
, m_SubModule(subModule)
, m_NexusGameID(0)
, m_GameName("")
, m_Endorse(false)
, m_Track(false)
, m_Hash(QByteArray())
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(UpdatePeriod period
, NexusInterface::NXMRequestInfo::Type type
, QVariant userData
, const QString &subModule
, MOBase::IPluginGame const *game
)
: m_ModID(0)
, m_ModVersion("0")
, m_FileID(0)
, m_Reply(nullptr)
, m_Type(type)
, m_UpdatePeriod(period)
, m_UserData(userData)
, m_Timeout(nullptr)
, m_Reroute(false)
, m_ID(s_NextID.fetchAndAddAcquire(1))
, m_URL(get_management_url())
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
, m_GameName(game->gameNexusName())
, m_Endorse(false)
, m_Track(false)
, m_Hash(QByteArray())
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(QByteArray &hash
, NexusInterface::NXMRequestInfo::Type type
, QVariant userData
, const QString &subModule
, MOBase::IPluginGame const *game
)
: m_ModID(0)
, m_ModVersion("0")
, m_FileID(0)
, m_Reply(nullptr)
, m_Type(type)
, m_UpdatePeriod(UpdatePeriod::NONE)
, m_UserData(userData)
, m_Timeout(nullptr)
, m_Reroute(false)
, m_ID(s_NextID.fetchAndAddAcquire(1))
, m_URL(get_management_url())
, m_SubModule(subModule)
, m_NexusGameID(game->nexusGameID())
, m_GameName(game->gameNexusName())
, m_Endorse(false)
, m_Track(false)
, m_Hash(hash)
{}