Files
yourls-ui/yourlsclient.cpp
2026-10-01 12:21:40 -04:00

149 lines
6.6 KiB
C++

#include "yourlsclient.h"
#include <QJsonDocument>
#include <QJsonObject>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QUrlQuery>
#ifndef QT_NO_DEBUG
#include <QDebug>
#endif
/**
* @brief Constructs the client.
* @param parent QObject parent.
*/
YourlsClient::YourlsClient(QObject *parent)
: QObject(parent), m_networkAccessManager(new QNetworkAccessManager(this)) {
}
/**
* @brief Requests a short URL from the YOURLS API.
* @param settings API endpoint and signature.
* @param longUrl Source URL to shorten.
*/
void YourlsClient::shortenUrl(const YourlsSettings &settings, const QUrl &longUrl) {
#ifndef QT_NO_DEBUG
qDebug() << "[yourls] source URL:" << longUrl << "valid:" << longUrl.isValid()
<< "scheme:" << longUrl.scheme() << "host:" << longUrl.host();
#endif
if (settings.apiUrl.trimmed().isEmpty()) {
emit shortenFailed(QStringLiteral("API URL is empty."));
return;
}
if (settings.signature.trimmed().isEmpty()) {
emit shortenFailed(QStringLiteral("API signature is empty."));
return;
}
if (!longUrl.isValid() || longUrl.scheme().isEmpty() || longUrl.host().isEmpty()) {
emit shortenFailed(QStringLiteral("Clipboard does not contain a valid URL."));
return;
}
QUrl requestUrl(settings.apiUrl);
if (!requestUrl.isValid()) {
emit shortenFailed(QStringLiteral("Configured API URL is invalid: %1").arg(settings.apiUrl));
return;
}
QUrlQuery query;
query.addQueryItem(QStringLiteral("signature"), settings.signature);
query.addQueryItem(QStringLiteral("action"), QStringLiteral("shorturl"));
query.addQueryItem(QStringLiteral("format"), QStringLiteral("json"));
query.addQueryItem(QStringLiteral("url"),
QString::fromLatin1(QUrl::toPercentEncoding(longUrl.toString(QUrl::FullyEncoded))));
requestUrl.setQuery(query);
QNetworkRequest request(requestUrl);
request.setHeader(QNetworkRequest::UserAgentHeader, QStringLiteral("yourls-ui/1.0"));
#ifndef QT_NO_DEBUG
// Never log the request query: it contains the API signature.
qDebug() << "[yourls] sending GET, endpoint:"
<< requestUrl.adjusted(QUrl::RemoveQuery | QUrl::RemoveFragment | QUrl::RemoveUserInfo)
<< "source URL:" << longUrl;
#endif
QNetworkReply *reply = m_networkAccessManager->get(request);
connect(reply, &QNetworkReply::finished, this, [this, reply, longUrl]() {
const QByteArray body = reply->readAll();
#ifndef QT_NO_DEBUG
qDebug() << "[yourls] response HTTP status:"
<< reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)
<< "network error code:" << reply->error()
<< "response bytes:" << body.size();
#endif
QJsonParseError parseError;
const QJsonDocument jsonDocument = QJsonDocument::fromJson(body, &parseError);
const QJsonObject object = jsonDocument.object();
const QUrl existingShortUrl(object.value(QStringLiteral("shorturl")).toString());
const QUrl existingLongUrl(object.value(QStringLiteral("url")).toObject().value(QStringLiteral("url")).toString());
// YOURLS reports duplicate URLs as HTTP 400, but returns the existing link.
if (reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt() == 400 && object.value(QStringLiteral("status")).toString() == QStringLiteral("fail") && object.value(QStringLiteral("code")).toString() == QStringLiteral("error:url") && existingLongUrl == longUrl && existingShortUrl.isValid() && (existingShortUrl.scheme() == QStringLiteral("https") || existingShortUrl.scheme() == QStringLiteral("http")) && !existingShortUrl.host().isEmpty()) {
#ifndef QT_NO_DEBUG
qDebug() << "[yourls] reusing existing short URL:" << existingShortUrl;
#endif
reply->deleteLater();
emit shortenSucceeded(existingShortUrl);
return;
}
if (reply->error() != QNetworkReply::NoError) {
const QString details = QStringLiteral("Network error: %1\nHTTP status: %2\nResponse body:\n%3")
.arg(reply->errorString(),
reply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toString(),
QString::fromUtf8(body));
reply->deleteLater();
emit shortenFailed(details);
return;
}
#ifndef QT_NO_DEBUG
qDebug() << "[yourls] JSON parse error:" << parseError.errorString()
<< "offset:" << parseError.offset << "is object:" << jsonDocument.isObject();
#endif
if (parseError.error != QJsonParseError::NoError || !jsonDocument.isObject()) {
const QString details = QStringLiteral("Failed to parse YOURLS response as JSON: %1\nResponse body:\n%2")
.arg(parseError.errorString(), QString::fromUtf8(body));
reply->deleteLater();
emit shortenFailed(details);
return;
}
const QString status = object.value(QStringLiteral("status")).toString();
const QString shortUrlText = object.value(QStringLiteral("shorturl")).toString();
#ifndef QT_NO_DEBUG
qDebug() << "[yourls] API status:" << status
<< "code:" << object.value(QStringLiteral("code")).toVariant()
<< "short URL:" << shortUrlText;
#endif
if (status.compare(QStringLiteral("success"), Qt::CaseInsensitive) != 0 || shortUrlText.isEmpty()) {
QStringList details;
details << QStringLiteral("YOURLS returned an error.");
if (object.contains(QStringLiteral("message")))
details << QStringLiteral("Message: %1").arg(object.value(QStringLiteral("message")).toString());
if (object.contains(QStringLiteral("code")))
details << QStringLiteral("Code: %1").arg(object.value(QStringLiteral("code")).toVariant().toString());
details << QStringLiteral("Raw response:\n%1").arg(QString::fromUtf8(body));
reply->deleteLater();
emit shortenFailed(details.join(QStringLiteral("\n")));
return;
}
const QUrl shortUrl(shortUrlText);
if (!shortUrl.isValid() || shortUrl.host().isEmpty() || (shortUrl.scheme() != QStringLiteral("https") && shortUrl.scheme() != QStringLiteral("http"))) {
reply->deleteLater();
emit shortenFailed(QStringLiteral("YOURLS returned an invalid short URL: %1").arg(shortUrlText));
return;
}
reply->deleteLater();
emit shortenSucceeded(shortUrl);
});
}