File indexing completed on 2024-05-12 16:28:04

0001 // SPDX-FileCopyrightText: 2021 kaniini <https://git.pleroma.social/kaniini>
0002 // SPDX-FileCopyrightText: 2021 Carl Schwan <carl@carlschwan.eu>
0003 // SPDX-License-Identifier: GPL-3.0-only
0004 
0005 #include "account.h"
0006 #include "accountconfig.h"
0007 #include "accountmanager.h"
0008 #include "network/networkcontroller.h"
0009 #include "notificationhandler.h"
0010 #include "preferences.h"
0011 #include "tokodon_debug.h"
0012 #include "tokodon_http_debug.h"
0013 #include "utils/utils.h"
0014 #include <QFileInfo>
0015 #include <QNetworkAccessManager>
0016 #if QT_VERSION < QT_VERSION_CHECK(6, 0, 0)
0017 #include <qt5keychain/keychain.h>
0018 #else
0019 #include <qt6keychain/keychain.h>
0020 #endif
0021 
0022 Account::Account(const QString &instanceUri, QNetworkAccessManager *nam, bool ignoreSslErrors, QObject *parent)
0023     : AbstractAccount(parent, instanceUri)
0024     , m_ignoreSslErrors(ignoreSslErrors)
0025     , m_qnam(nam)
0026 {
0027     m_preferences = new Preferences(this);
0028     setInstanceUri(instanceUri);
0029 
0030     auto notificationHandler = new NotificationHandler(m_qnam, this);
0031     connect(this, &Account::notification, notificationHandler, [this, notificationHandler](std::shared_ptr<Notification> notification) {
0032         notificationHandler->handle(notification, this);
0033     });
0034 }
0035 
0036 Account::Account(const AccountConfig &settings, QNetworkAccessManager *nam, QObject *parent)
0037     : AbstractAccount(parent)
0038     , m_qnam(nam)
0039 {
0040     m_preferences = new Preferences(this);
0041     auto notificationHandler = new NotificationHandler(m_qnam, this);
0042     connect(this, &Account::notification, notificationHandler, [this, notificationHandler](std::shared_ptr<Notification> notification) {
0043         notificationHandler->handle(notification, this);
0044     });
0045 
0046     buildFromSettings(settings);
0047 }
0048 
0049 Account::~Account()
0050 {
0051     m_identityCache.clear();
0052 }
0053 
0054 void Account::get(const QUrl &url,
0055                   bool authenticated,
0056                   QObject *parent,
0057                   std::function<void(QNetworkReply *)> reply_cb,
0058                   std::function<void(QNetworkReply *)> errorCallback)
0059 {
0060     QNetworkRequest request = makeRequest(url, authenticated);
0061     qCDebug(TOKODON_HTTP) << "GET" << url;
0062 
0063     QNetworkReply *reply = m_qnam->get(request);
0064     reply->setParent(parent);
0065     handleReply(reply, reply_cb, errorCallback);
0066 }
0067 
0068 void Account::post(const QUrl &url,
0069                    const QJsonDocument &doc,
0070                    bool authenticated,
0071                    QObject *parent,
0072                    std::function<void(QNetworkReply *)> reply_cb,
0073                    std::function<void(QNetworkReply *)> error_cb,
0074                    QHash<QByteArray, QByteArray> headers)
0075 {
0076     auto post_data = doc.toJson();
0077 
0078     QNetworkRequest request = makeRequest(url, authenticated);
0079     request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
0080     for (const auto [headerKey, headerValue] : asKeyValueRange(headers)) {
0081         request.setRawHeader(headerKey, headerValue);
0082     }
0083     qCDebug(TOKODON_HTTP) << "POST" << url << "[" << post_data << "]";
0084 
0085     auto reply = m_qnam->post(request, post_data);
0086     reply->setParent(parent);
0087     handleReply(reply, reply_cb, error_cb);
0088 }
0089 
0090 void Account::put(const QUrl &url, const QJsonDocument &doc, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> reply_cb)
0091 {
0092     auto post_data = doc.toJson();
0093 
0094     QNetworkRequest request = makeRequest(url, authenticated);
0095     request.setHeader(QNetworkRequest::ContentTypeHeader, "application/json");
0096     qCDebug(TOKODON_HTTP) << "PUT" << url << "[" << post_data << "]";
0097 
0098     QNetworkReply *reply = m_qnam->put(request, post_data);
0099     reply->setParent(parent);
0100     handleReply(reply, reply_cb);
0101 }
0102 
0103 void Account::post(const QUrl &url, const QUrlQuery &formdata, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> reply_cb)
0104 {
0105     auto post_data = formdata.toString().toLatin1();
0106 
0107     QNetworkRequest request = makeRequest(url, authenticated);
0108     request.setHeader(QNetworkRequest::ContentTypeHeader, "application/x-www-form-urlencoded");
0109     qCDebug(TOKODON_HTTP) << "POST" << url << "[" << post_data << "]";
0110 
0111     QNetworkReply *reply = m_qnam->post(request, post_data);
0112     reply->setParent(parent);
0113     handleReply(reply, reply_cb);
0114 }
0115 
0116 QNetworkReply *Account::post(const QUrl &url, QHttpMultiPart *message, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> reply_cb)
0117 {
0118     QNetworkRequest request = makeRequest(url, authenticated);
0119 
0120     qCDebug(TOKODON_HTTP) << "POST" << url << "(multipart-message)";
0121 
0122     QNetworkReply *reply = m_qnam->post(request, message);
0123     reply->setParent(parent);
0124     handleReply(reply, reply_cb);
0125     return reply;
0126 }
0127 
0128 void Account::patch(const QUrl &url, QHttpMultiPart *multiPart, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> callback)
0129 {
0130     QNetworkRequest request = makeRequest(url, authenticated);
0131     qCDebug(TOKODON_HTTP) << "PATCH" << url << "(multipart-message)";
0132 
0133     QNetworkReply *reply = m_qnam->sendCustomRequest(request, "PATCH", multiPart);
0134     reply->setParent(parent);
0135     handleReply(reply, callback);
0136 }
0137 
0138 void Account::deleteResource(const QUrl &url, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> callback)
0139 {
0140     QNetworkRequest request = makeRequest(url, authenticated);
0141 
0142     qCDebug(TOKODON_HTTP) << "DELETE" << url << "(multipart-message)";
0143 
0144     QNetworkReply *reply = m_qnam->deleteResource(request);
0145     reply->setParent(parent);
0146     handleReply(reply, callback);
0147 }
0148 
0149 QNetworkRequest Account::makeRequest(const QUrl &url, bool authenticated) const
0150 {
0151     QNetworkRequest request(url);
0152 
0153     if (authenticated && haveToken()) {
0154         const QByteArray bearer = QString("Bearer " + m_token).toLocal8Bit();
0155         request.setRawHeader("Authorization", bearer);
0156     }
0157 
0158     return request;
0159 }
0160 
0161 void Account::handleReply(QNetworkReply *reply, std::function<void(QNetworkReply *)> reply_cb, std::function<void(QNetworkReply *)> errorCallback) const
0162 {
0163     connect(reply, &QNetworkReply::finished, [reply, reply_cb, errorCallback]() {
0164         reply->deleteLater();
0165         if (200 != reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) && !reply->url().toString().contains("nodeinfo")) {
0166             qCWarning(TOKODON_LOG) << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) << reply->url();
0167             if (errorCallback) {
0168                 errorCallback(reply);
0169             } else {
0170                 Q_EMIT NetworkController::instance().networkErrorOccurred(reply->errorString());
0171             }
0172             return;
0173         }
0174         if (reply_cb) {
0175             reply_cb(reply);
0176         }
0177     });
0178     if (m_ignoreSslErrors) {
0179         connect(reply, &QNetworkReply::sslErrors, this, [reply](const QList<QSslError> &) {
0180             reply->ignoreSslErrors();
0181         });
0182     }
0183 }
0184 
0185 // assumes file is already opened and named
0186 QNetworkReply *Account::upload(const QUrl &filename, std::function<void(QNetworkReply *)> callback)
0187 {
0188     auto file = new QFile(filename.toLocalFile());
0189     const QFileInfo info(filename.toLocalFile());
0190     file->open(QFile::ReadOnly);
0191 
0192     auto mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
0193 
0194     QHttpPart filePart;
0195     filePart.setHeader(QNetworkRequest::ContentTypeHeader, "application/octet-stream");
0196     filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(info.fileName()));
0197     filePart.setBodyDevice(file);
0198     file->setParent(mp);
0199 
0200     mp->append(filePart);
0201 
0202     const auto uploadUrl = apiUrl("/api/v1/media");
0203     qCDebug(TOKODON_HTTP) << "POST" << uploadUrl << "(upload)";
0204 
0205     return post(uploadUrl, mp, true, this, callback);
0206 }
0207 
0208 static QMap<QString, AbstractAccount::StreamingEventType> stringToStreamingEventType = {
0209     {"update", AbstractAccount::StreamingEventType::UpdateEvent},
0210     {"delete", AbstractAccount::StreamingEventType::DeleteEvent},
0211     {"notification", AbstractAccount::StreamingEventType::NotificationEvent},
0212     {"filters_changed", AbstractAccount::StreamingEventType::FiltersChangedEvent},
0213     {"conversation", AbstractAccount::StreamingEventType::ConversationEvent},
0214     {"announcement", AbstractAccount::StreamingEventType::AnnouncementEvent},
0215     {"announcement.reaction", AbstractAccount::StreamingEventType::AnnouncementRedactedEvent},
0216     {"announcement.delete", AbstractAccount::StreamingEventType::AnnouncementDeletedEvent},
0217     {"status.update", AbstractAccount::StreamingEventType::StatusUpdatedEvent},
0218     {"encrypted_message", AbstractAccount::StreamingEventType::EncryptedMessageChangedEvent},
0219 };
0220 
0221 QWebSocket *Account::streamingSocket(const QString &stream)
0222 {
0223     if (m_websockets.contains(stream)) {
0224         return m_websockets[stream];
0225     }
0226 
0227     auto socket = new QWebSocket();
0228     socket->setParent(this);
0229 
0230     const auto url = streamingUrl(stream);
0231 
0232     connect(socket, &QWebSocket::textMessageReceived, this, [=](const QString &message) {
0233         const auto env = QJsonDocument::fromJson(message.toLocal8Bit());
0234 
0235         const auto event = stringToStreamingEventType[env.object()["event"].toString()];
0236         Q_EMIT streamingEvent(event, env.object()["payload"].toString().toLocal8Bit());
0237 
0238         if (event == NotificationEvent) {
0239             const auto doc = QJsonDocument::fromJson(env.object()["payload"].toString().toLocal8Bit());
0240             handleNotification(doc);
0241             return;
0242         }
0243     });
0244 
0245     socket->open(url);
0246 
0247     m_websockets[stream] = socket;
0248     return socket;
0249 }
0250 
0251 void Account::validateToken()
0252 {
0253     const QUrl verify_credentials = apiUrl("/api/v1/accounts/verify_credentials");
0254 
0255     get(
0256         verify_credentials,
0257         true,
0258         this,
0259         [=](QNetworkReply *reply) {
0260             if (!reply->isFinished()) {
0261                 return;
0262             }
0263 
0264             const auto doc = QJsonDocument::fromJson(reply->readAll());
0265 
0266             if (!doc.isObject()) {
0267                 return;
0268             }
0269 
0270             const auto object = doc.object();
0271             if (!object.contains("source")) {
0272                 return;
0273             }
0274 
0275             m_identity = identityLookup(object["id"].toString(), object);
0276             m_name = m_identity->username();
0277             Q_EMIT identityChanged();
0278             Q_EMIT authenticated(true);
0279         },
0280         [=](QNetworkReply *) {
0281             Q_EMIT authenticated(false);
0282         });
0283 
0284     fetchInstanceMetadata();
0285 
0286     // set up streaming for notifications
0287     streamingSocket("user");
0288 }
0289 
0290 void Account::writeToSettings()
0291 {
0292     // do not write to settings if we do not have complete information yet,
0293     // or else it writes malformed and possibly duplicate accounts to settings.
0294     if (m_name.isEmpty() || m_instance_uri.isEmpty()) {
0295         return;
0296     }
0297 
0298     AccountConfig config(settingsGroupName());
0299     config.setClientId(m_client_id);
0300     config.setInstanceUri(m_instance_uri);
0301     config.setName(m_name);
0302     config.setIgnoreSslErrors(m_ignoreSslErrors);
0303 
0304     config.save();
0305 
0306     auto accessTokenJob = new QKeychain::WritePasswordJob{"Tokodon", this};
0307 #ifdef SAILFISHOS
0308     accessTokenJob->setInsecureFallback(true);
0309 #endif
0310     accessTokenJob->setKey(accessTokenKey());
0311     accessTokenJob->setTextData(m_token);
0312     accessTokenJob->start();
0313 
0314     auto clientSecretJob = new QKeychain::WritePasswordJob{"Tokodon", this};
0315 #ifdef SAILFISHOS
0316     clientSecretJob->setInsecureFallback(true);
0317 #endif
0318     clientSecretJob->setKey(clientSecretKey());
0319     clientSecretJob->setTextData(m_client_secret);
0320     clientSecretJob->start();
0321 }
0322 
0323 void Account::buildFromSettings(const AccountConfig &settings)
0324 {
0325     m_client_id = settings.clientId();
0326     m_name = settings.name();
0327     m_instance_uri = settings.instanceUri();
0328     m_ignoreSslErrors = settings.ignoreSslErrors();
0329 
0330     auto accessTokenJob = new QKeychain::ReadPasswordJob{"Tokodon", this};
0331 #ifdef SAILFISHOS
0332     accessTokenJob->setInsecureFallback(true);
0333 #endif
0334     accessTokenJob->setKey(accessTokenKey());
0335 
0336     QObject::connect(accessTokenJob, &QKeychain::ReadPasswordJob::finished, [this, accessTokenJob]() {
0337         m_token = accessTokenJob->textData();
0338 
0339         validateToken();
0340     });
0341 
0342     accessTokenJob->start();
0343 
0344     auto clientSecretJob = new QKeychain::ReadPasswordJob{"Tokodon", this};
0345 #ifdef SAILFISHOS
0346     clientSecretJob->setInsecureFallback(true);
0347 #endif
0348     clientSecretJob->setKey(clientSecretKey());
0349 
0350     QObject::connect(clientSecretJob, &QKeychain::ReadPasswordJob::finished, [this, clientSecretJob]() {
0351         m_client_secret = clientSecretJob->textData();
0352     });
0353 
0354     clientSecretJob->start();
0355 }
0356 
0357 bool Account::hasFollowRequests() const
0358 {
0359     return m_hasFollowRequests;
0360 }
0361 
0362 void Account::checkForFollowRequests()
0363 {
0364     get(apiUrl("/api/v1/follow_requests"), true, this, [this](QNetworkReply *reply) {
0365         const auto followRequestResult = QJsonDocument::fromJson(reply->readAll());
0366         const bool hasFollowRequests = followRequestResult.isArray() && !followRequestResult.array().isEmpty();
0367         if (hasFollowRequests != m_hasFollowRequests) {
0368             m_hasFollowRequests = hasFollowRequests;
0369             Q_EMIT hasFollowRequestsChanged();
0370         }
0371     });
0372 }