/*
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 .
*/
#include "nexusinterface.h"
#include "bbcode.h"
#include "iplugingame.h"
#include "nxmaccessmanager.h"
#include "selectiondialog.h"
#include "settings.h"
#include "shared/util.h"
#include
#include
#include
#include
#include
#include
#include
#include
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::requestGameInfo(QString gameName, QVariant userData)
{
m_RequestIDs.insert(
m_Interface->requestGameInfo(gameName, this, userData, m_SubModule));
}
void NexusBridge::nxmDescriptionAvailable(QString gameName, int modID,
QVariant userData, QVariant resultData,
int requestID)
{
std::set::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::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
QList 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();
temp.author = fileInfo["author"].toString();
temp.uploader = fileInfo["uploaded_by"].toString();
temp.uploaderUrl = fileInfo["uploaded_users_profile_url"].toString();
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::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::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::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::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::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::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit trackingToggled(gameName, modID, userData, tracked);
}
}
void NexusBridge::nxmGameInfoAvailable(QString gameName, QVariant userData,
QVariant resultData, int requestID)
{
std::set::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit gameInfoAvailable(gameName, userData, resultData);
}
}
void NexusBridge::nxmRequestFailed(QString gameName, int modID, int fileID,
QVariant userData, int requestID, int errorCode,
const QString& errorMessage)
{
std::set::iterator iter = m_RequestIDs.find(requestID);
if (iter != m_RequestIDs.end()) {
m_RequestIDs.erase(iter);
emit requestFailed(gameName, modID, fileID, userData, errorCode, 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& 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_AccessManager = new NXMAccessManager(this, s, createVersionInfo().string());
m_DiskCache = new QNetworkDiskCache(this);
connect(m_AccessManager, &NXMAccessManager::requestNXMDownload, this,
&NexusInterface::downloadRequestedNXM);
connect(m_AccessManager, &NXMAccessManager::credentialsReceived, this,
&NexusInterface::setUserAccount);
}
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(QRegularExpression("[^0-9]"), Qt::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) {
QString gameNexusName = game->gameNexusName().toLower();
if (gameNexusName.isEmpty()) {
return "";
} else {
return "https://www.nexusmods.com/" + gameNexusName;
}
} else {
log::error("getGameURL can't find plugin for {}", gameName);
return "";
}
}
QString NexusInterface::getOldModsURL(const 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>
NexusInterface::getGameChoices(const MOBase::IPluginGame* game)
{
std::vector> choices;
choices.push_back(
std::pair(game->gameShortName(), game->gameName()));
for (QString gameName : game->validShortNames()) {
for (auto gamePlugin : m_PluginContainer->plugins()) {
if (gamePlugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
choices.push_back(std::pair(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)
{
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_DESCRIPTION, userData,
subModule, getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestModInfo(QString gameName, int modID, QObject* receiver,
QVariant userData, const QString& subModule)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_MODINFO, userData, subModule,
getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestUpdateInfo(QString gameName,
NexusInterface::UpdatePeriod period,
QObject* receiver, QVariant userData,
const QString& subModule)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(period, NXMRequestInfo::TYPE_CHECKUPDATES, userData,
subModule, getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, 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->gameNexusName());
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, 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";
fileMap["author"] = "fakeAuthor";
fileMap["uploaded_by"] = "fakeUploader";
fileMap["uploaded_users_profile_url"] = "https://fakeuploader.com";
result.append(fileMap);
emit nxmFilesAvailable("fakeGame", 1234, "fake", result, id++);
}
int NexusInterface::requestFiles(QString gameName, int modID, QObject* receiver,
QVariant userData, const QString& subModule)
{
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_FILES, userData, subModule,
getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestFileInfo(QString gameName, int modID, int fileID,
QObject* receiver, QVariant userData,
const QString& subModule)
{
NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_FILEINFO, userData,
subModule, getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestDownloadURL(QString gameName, int modID, int fileID,
QObject* receiver, QVariant userData,
const QString& subModule)
{
NXMRequestInfo requestInfo(modID, fileID, NXMRequestInfo::TYPE_DOWNLOADURL, userData,
subModule, getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, 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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, 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)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(modID, modVersion, NXMRequestInfo::TYPE_TOGGLEENDORSEMENT,
userData, subModule, getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, 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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestToggleTracking(QString gameName, int modID, bool track,
QObject* receiver, QVariant userData,
const QString& subModule)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(modID, NXMRequestInfo::TYPE_TOGGLETRACKING, userData,
subModule, getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestGameInfo(QString gameName, QObject* receiver,
QVariant userData, const QString& subModule)
{
if (m_User.shouldThrottle()) {
throttledWarning(m_User);
return -1;
}
NXMRequestInfo requestInfo(NXMRequestInfo::TYPE_GAMEINFO, userData, subModule,
getGameNexusName(gameName));
m_RequestQueue.enqueue(requestInfo);
connect(this, SIGNAL(nxmGameInfoAvailable(QString, QVariant, QVariant, int)),
receiver, SLOT(nxmGameInfoAvailable(QString, QVariant, QVariant, int)),
Qt::UniqueConnection);
connect(
this, SIGNAL(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
int NexusInterface::requestInfoFromMd5(QString gameName, QByteArray& hash,
QObject* receiver, QVariant userData,
const QString& subModule)
{
NXMRequestInfo requestInfo(hash, NXMRequestInfo::TYPE_FILEINFO_MD5, userData,
subModule, getGameNexusName(gameName));
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, int, QString)),
receiver, SLOT(nxmRequestFailed(QString, int, int, QVariant, int, int, QString)),
Qt::UniqueConnection);
nextRequest();
return requestInfo.m_ID;
}
IPluginGame* NexusInterface::getGame(const QString& gameName) const
{
const auto& gamePlugins = m_PluginContainer->plugins();
IPluginGame* gamePlugin = nullptr;
for (auto plugin : gamePlugins) {
if (plugin != nullptr &&
plugin->gameShortName().compare(gameName, Qt::CaseInsensitive) == 0) {
gamePlugin = plugin;
break;
}
}
return gamePlugin;
}
QString NexusInterface::getGameNexusName(const QString& gameName) const
{
IPluginGame* game = getGame(gameName);
if (game != nullptr) {
return game->gameNexusName();
}
return gameName;
}
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."));
}
}
const auto currentTokens = m_AccessManager->tokens();
if (!currentTokens ||
(currentTokens->accessToken.isEmpty() && currentTokens->apiKey.isEmpty())) {
log::error("nexus: no OAuth token available, request aborted");
return;
}
if (!currentTokens->accessToken.isEmpty()) {
if (currentTokens->isExpired()) {
m_AccessManager->connectOrRefresh(*currentTokens);
return;
}
}
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(
qvariant_cast(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()));
} break;
case NXMRequestInfo::TYPE_GAMEINFO: {
url = QStringLiteral("%1/games/%2").arg(info.m_URL).arg(info.m_GameName);
}
}
} else {
url = info.m_URL;
}
QNetworkRequest request(url);
if (!currentTokens->accessToken.isEmpty()) {
if (postData.object().isEmpty()) {
if (!requestIsDelete) {
info.m_Reply = m_AccessManager->makeOAuthGetRequest(url);
} else {
m_AccessManager->addAPIHeaders(request);
info.m_Reply = m_AccessManager->makeOAuthDeleteRequest(request);
}
} else if (!requestIsDelete) {
info.m_Reply = m_AccessManager->makeOAuthPostRequest(url, postData.toJson());
} else {
// Qt doesn't support DELETE with a payload as that's technically against the HTTP
// standard...
info.m_Reply =
m_AccessManager->makeOAuthCustomRequest(request, "DELETE", postData.toJson());
}
} else {
request.setRawHeader("APIKEY", currentTokens->apiKey.toUtf8());
m_AccessManager->addAPIHeaders(request);
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(errorOccurred(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::iterator iter)
{
QNetworkReply* reply = iter->m_Reply;
auto error = reply->error();
if (error != QNetworkReply::NoError) {
int statusCode = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt();
QString errorMsg = reply->errorString();
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: {}", errorMsg);
} else {
QByteArray data = reply->readAll();
if (!data.isEmpty()) {
QJsonDocument responseDoc = QJsonDocument::fromJson(data);
if (!responseDoc.isNull()) {
auto result = responseDoc.toVariant().toMap();
auto error = result.find("error");
if (error != result.end())
errorMsg = result.value("error").toString();
}
}
}
emit nxmRequestFailed(iter->m_GameName, iter->m_ModID, iter->m_FileID,
iter->m_UserData, iter->m_ID, statusCode, errorMsg);
} 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;
case NXMRequestInfo::TYPE_GAMEINFO: {
emit nxmGameInfoAvailable(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(sender());
for (std::list::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(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(sender());
if (timer == nullptr) {
log::warn("invalid sender type");
return;
}
for (std::list::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";
}
} // namespace
NexusInterface::NXMRequestInfo::NXMRequestInfo(
int modID, NexusInterface::NXMRequestInfo::Type type, QVariant userData,
const QString& subModule, const QString& gameNexusName)
: 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_GameName(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, const QString& gameNexusName)
: 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_GameName(gameNexusName),
m_Endorse(false), m_Track(false), m_Hash(QByteArray())
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(Type type, QVariant userData,
const QString& subModule,
const QString& gameNexusName)
: 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_GameName(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, const QString& gameNexusName)
: 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_GameName(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_GameName(""),
m_Endorse(false), m_Track(false), m_Hash(QByteArray())
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(
UpdatePeriod period, NexusInterface::NXMRequestInfo::Type type, QVariant userData,
const QString& subModule, const QString& gameNexusName)
: 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_GameName(gameNexusName),
m_Endorse(false), m_Track(false), m_Hash(QByteArray())
{}
NexusInterface::NXMRequestInfo::NXMRequestInfo(
QByteArray& hash, NexusInterface::NXMRequestInfo::Type type, QVariant userData,
const QString& subModule, const QString& gameNexusName)
: 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_GameName(gameNexusName),
m_Endorse(false), m_Track(false), m_Hash(hash)
{}