#include "tapped_out_interface.h" #include "deck_analysis_url.h" #include "libcockatrice/card/card_info.h" #include "libcockatrice/card/database/card_database_querier.h" #include "libcockatrice/deck_list/tree/inner_deck_list_node.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include TappedOutInterface::TappedOutInterface(QObject *parent) : QObject(parent) { manager = NetworkManagerFactory::create(this); connect(manager, &QNetworkAccessManager::finished, this, &TappedOutInterface::queryFinished); } void TappedOutInterface::showError(const QString &message) { // The notice belongs to the originating editor, not this one-shot request. // Retire the request before opening UI: opening a window may reenter Qt. QWidget *owner = qobject_cast(parent()); auto *notice = new QMessageBox(QMessageBox::Critical, tr("Deck analysis"), message, QMessageBox::Ok, owner); notice->setTextFormat(Qt::PlainText); notice->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard); notice->setAttribute(Qt::WA_DeleteOnClose); deleteLater(); notice->open(); } void TappedOutInterface::queryFinished(QNetworkReply *reply) { if (replyHandled) { return; } replyHandled = true; // All terminal paths release the reply before showing UI or invoking an // external URL handler, either of which may destroy the originating editor. reply->deleteLater(); if (reply->error() != QNetworkReply::NoError) { showError(reply->errorString()); return; } const int httpStatus = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); if (httpStatus >= 200 && httpStatus < 400 && reply->hasRawHeader("Location")) { const QByteArray location = reply->rawHeader("Location"); QUrl deckUrl = reply->url().resolved(QUrl::fromEncoded(location, QUrl::StrictMode)); if (location.trimmed().isEmpty() || !normalizeAnalysisUrl(deckUrl, QStringLiteral("tappedout.net"))) { showError(tr("The reply from TappedOut contained an invalid deck address.")); return; } QPointer guard(this); deleteLater(); if (!QDesktopServices::openUrl(deckUrl) && guard) { showError(tr("Could not open your browser. Open this address to see the analyzed deck:\n%1") .arg(deckUrl.toDisplayString())); } return; } const QString data = QString::fromUtf8(reply->readAll()); QStringList errorMessageList = {tr("Unable to analyze the deck on TappedOut.")}; static const QRegularExpression errorsRx("
(.*?)", QRegularExpression::DotMatchesEverythingOption); const auto match = errorsRx.match(data); if (match.hasMatch()) { static const QRegularExpression itemRx("
  • (.*?)
  • ", QRegularExpression::DotMatchesEverythingOption); auto items = itemRx.globalMatch(match.captured(1)); while (items.hasNext()) { const QString error = QTextDocumentFragment::fromHtml(items.next().captured(1)).toPlainText().simplified(); if (!error.isEmpty()) { errorMessageList.append(error); } } } qCWarning(TappedOutInterfaceLog) << "TappedOut analysis failed, HTTP status" << httpStatus; showError(errorMessageList.join('\n')); } void TappedOutInterface::getAnalyzeRequestData(const DeckList &deck, QByteArray &data) { DeckList mainboard, sideboard; copyDeckSplitMainAndSide(deck, mainboard, sideboard); data.append("name=" + QUrl::toPercentEncoding(deck.getName())); data.append("&mainboard=" + QUrl::toPercentEncoding(mainboard.writeToString_Plain(false, true))); data.append("&sideboard=" + QUrl::toPercentEncoding(sideboard.writeToString_Plain(false, true))); } void TappedOutInterface::analyzeDeck(const DeckList &deck) { QByteArray data; getAnalyzeRequestData(deck, data); QNetworkRequest request(QUrl("https://tappedout.net/mtg-decks/paste/")); request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded"); request.setHeader(QNetworkRequest::UserAgentHeader, QString("Cockatrice %1").arg(VERSION_STRING)); // we interpret the redirect and open it in the browser instead, do not follow redirects request.setAttribute(QNetworkRequest::RedirectPolicyAttribute, QNetworkRequest::ManualRedirectPolicy); manager->post(request, data); } void TappedOutInterface::copyDeckSplitMainAndSide(const DeckList &source, DeckList &mainboard, DeckList &sideboard) { auto copyMainOrSide = [&mainboard, &sideboard](const auto node, const auto card) { CardInfoPtr dbCard = CardDatabaseManager::query()->getCardInfo(card->getName()); // Exclude confirmed tokens, but preserve cards missing from the local // database, including newly released cards in an imported deck. if ((dbCard && dbCard->getIsToken()) || node->getName() == DECK_ZONE_MAYBEBOARD) { return; } DecklistCardNode *addedCard; if (node->getName() == DECK_ZONE_SIDE) { addedCard = sideboard.addCard(card->getName(), node->getName(), -1); } else { addedCard = mainboard.addCard(card->getName(), node->getName(), -1); } addedCard->setNumber(card->getNumber()); }; source.forEachCard(copyMainOrSide); }