File indexing completed on 2024-05-12 05:04:08

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 
0007 #include "networkcontroller.h"
0008 #include "notificationhandler.h"
0009 #include "tokodon_http_debug.h"
0010 #include "utils.h"
0011 
0012 #ifdef HAVE_KUNIFIEDPUSH
0013 #include "ecdh.h"
0014 #include "tokodon_debug.h"
0015 #endif
0016 
0017 #include <qt6keychain/keychain.h>
0018 
0019 using namespace Qt::Literals::StringLiterals;
0020 
0021 Account::Account(const QString &instanceUri, QNetworkAccessManager *nam, bool ignoreSslErrors, bool admin, QObject *parent)
0022     : AbstractAccount(parent, instanceUri)
0023     , m_ignoreSslErrors(ignoreSslErrors)
0024     , m_qnam(nam)
0025 {
0026     m_preferences = new Preferences(this);
0027     setInstanceUri(instanceUri);
0028     registerApplication(QStringLiteral("Tokodon"),
0029                         QStringLiteral("https://apps.kde.org/tokodon"),
0030                         admin ? QStringLiteral("admin:read admin:write") : QStringLiteral(""));
0031 
0032     auto notificationHandler = new NotificationHandler(m_qnam, this);
0033     connect(this, &Account::notification, notificationHandler, [this, notificationHandler](std::shared_ptr<Notification> notification) {
0034         notificationHandler->handle(notification, this);
0035     });
0036 }
0037 
0038 Account::Account(AccountConfig *settings, QNetworkAccessManager *nam, QObject *parent)
0039     : AbstractAccount(parent)
0040     , m_qnam(nam)
0041 {
0042     m_preferences = new Preferences(this);
0043     auto notificationHandler = new NotificationHandler(m_qnam, this);
0044     connect(this, &Account::notification, notificationHandler, [this, notificationHandler](std::shared_ptr<Notification> notification) {
0045         notificationHandler->handle(notification, this);
0046     });
0047 
0048     m_config = settings;
0049     connect(this, &Account::authenticated, this, &Account::checkForFollowRequests);
0050     buildFromSettings();
0051 }
0052 
0053 Account::~Account()
0054 {
0055     m_identityCache.clear();
0056 }
0057 
0058 void Account::get(const QUrl &url,
0059                   bool authenticated,
0060                   QObject *parent,
0061                   std::function<void(QNetworkReply *)> reply_cb,
0062                   std::function<void(QNetworkReply *)> errorCallback)
0063 {
0064     QNetworkRequest request = makeRequest(url, authenticated);
0065     qCDebug(TOKODON_HTTP) << "GET" << url;
0066 
0067     QNetworkReply *reply = m_qnam->get(request);
0068     reply->setParent(parent);
0069     handleReply(reply, reply_cb, errorCallback);
0070 }
0071 
0072 void Account::post(const QUrl &url,
0073                    const QJsonDocument &doc,
0074                    bool authenticated,
0075                    QObject *parent,
0076                    std::function<void(QNetworkReply *)> reply_cb,
0077                    std::function<void(QNetworkReply *)> error_cb,
0078                    QHash<QByteArray, QByteArray> headers)
0079 {
0080     auto post_data = doc.toJson();
0081 
0082     QNetworkRequest request = makeRequest(url, authenticated);
0083     request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
0084     for (const auto [headerKey, headerValue] : asKeyValueRange(headers)) {
0085         request.setRawHeader(headerKey, headerValue);
0086     }
0087     qCDebug(TOKODON_HTTP) << "POST" << url << "[" << post_data << "]";
0088 
0089     auto reply = m_qnam->post(request, post_data);
0090     reply->setParent(parent);
0091     handleReply(reply, reply_cb, error_cb);
0092 }
0093 
0094 void Account::put(const QUrl &url, const QJsonDocument &doc, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> reply_cb)
0095 {
0096     auto post_data = doc.toJson();
0097 
0098     QNetworkRequest request = makeRequest(url, authenticated);
0099     request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/json"));
0100     qCDebug(TOKODON_HTTP) << "PUT" << url << "[" << post_data << "]";
0101 
0102     QNetworkReply *reply = m_qnam->put(request, post_data);
0103     reply->setParent(parent);
0104     handleReply(reply, reply_cb);
0105 }
0106 
0107 void Account::put(const QUrl &url, const QUrlQuery &formdata, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> reply_cb)
0108 {
0109     auto post_data = formdata.toString().toLatin1();
0110 
0111     QNetworkRequest request = makeRequest(url, authenticated);
0112     request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
0113     qCDebug(TOKODON_HTTP) << "PUT" << url << "[" << post_data << "]";
0114 
0115     QNetworkReply *reply = m_qnam->put(request, post_data);
0116     reply->setParent(parent);
0117     handleReply(reply, reply_cb);
0118 }
0119 
0120 void Account::post(const QUrl &url,
0121                    const QUrlQuery &formdata,
0122                    bool authenticated,
0123                    QObject *parent,
0124                    std::function<void(QNetworkReply *)> reply_cb,
0125                    std::function<void(QNetworkReply *)> errorCallback)
0126 {
0127     auto post_data = formdata.toString().toLatin1();
0128 
0129     QNetworkRequest request = makeRequest(url, authenticated);
0130     request.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/x-www-form-urlencoded"));
0131     qCDebug(TOKODON_HTTP) << "POST" << url << "[" << post_data << "]";
0132 
0133     QNetworkReply *reply = m_qnam->post(request, post_data);
0134     reply->setParent(parent);
0135     handleReply(reply, reply_cb, errorCallback);
0136 }
0137 
0138 QNetworkReply *Account::post(const QUrl &url, QHttpMultiPart *message, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> reply_cb)
0139 {
0140     QNetworkRequest request = makeRequest(url, authenticated);
0141 
0142     qCDebug(TOKODON_HTTP) << "POST" << url << "(multipart-message)";
0143 
0144     QNetworkReply *reply = m_qnam->post(request, message);
0145     reply->setParent(parent);
0146     handleReply(reply, reply_cb);
0147     return reply;
0148 }
0149 
0150 void Account::patch(const QUrl &url, QHttpMultiPart *multiPart, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> callback)
0151 {
0152     QNetworkRequest request = makeRequest(url, authenticated);
0153     qCDebug(TOKODON_HTTP) << "PATCH" << url << "(multipart-message)";
0154 
0155     QNetworkReply *reply = m_qnam->sendCustomRequest(request, "PATCH", multiPart);
0156     reply->setParent(parent);
0157     handleReply(reply, callback);
0158 }
0159 
0160 void Account::deleteResource(const QUrl &url, bool authenticated, QObject *parent, std::function<void(QNetworkReply *)> callback)
0161 {
0162     QNetworkRequest request = makeRequest(url, authenticated);
0163 
0164     qCDebug(TOKODON_HTTP) << "DELETE" << url << "(multipart-message)";
0165 
0166     QNetworkReply *reply = m_qnam->deleteResource(request);
0167     reply->setParent(parent);
0168     handleReply(reply, callback);
0169 }
0170 
0171 QNetworkRequest Account::makeRequest(const QUrl &url, bool authenticated) const
0172 {
0173     QNetworkRequest request(url);
0174 
0175     if (authenticated && haveToken()) {
0176         const QByteArray bearer = QStringLiteral("Bearer %1").arg(m_token).toLocal8Bit();
0177         request.setRawHeader("Authorization", bearer);
0178     }
0179 
0180     return request;
0181 }
0182 
0183 void Account::handleReply(QNetworkReply *reply, std::function<void(QNetworkReply *)> reply_cb, std::function<void(QNetworkReply *)> errorCallback) const
0184 {
0185     connect(reply, &QNetworkReply::finished, [reply, reply_cb, errorCallback]() {
0186         reply->deleteLater();
0187         if (200 != reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) && !reply->url().toString().contains("nodeinfo"_L1)) {
0188             qCWarning(TOKODON_HTTP) << reply->attribute(QNetworkRequest::HttpStatusCodeAttribute) << reply->url();
0189             if (errorCallback) {
0190                 errorCallback(reply);
0191             } else {
0192                 Q_EMIT NetworkController::instance().networkErrorOccurred(reply->errorString());
0193             }
0194             return;
0195         }
0196         if (reply_cb) {
0197             reply_cb(reply);
0198         }
0199     });
0200     if (m_ignoreSslErrors) {
0201         connect(reply, &QNetworkReply::sslErrors, this, [reply](const QList<QSslError> &) {
0202             reply->ignoreSslErrors();
0203         });
0204     }
0205 }
0206 
0207 // assumes file is already opened and named
0208 QNetworkReply *Account::upload(const QUrl &filename, std::function<void(QNetworkReply *)> callback)
0209 {
0210     auto file = new QFile(filename.toLocalFile());
0211     const QFileInfo info(filename.toLocalFile());
0212     file->open(QFile::ReadOnly);
0213 
0214     auto mp = new QHttpMultiPart(QHttpMultiPart::FormDataType);
0215 
0216     QHttpPart filePart;
0217     filePart.setHeader(QNetworkRequest::ContentTypeHeader, QStringLiteral("application/octet-stream"));
0218     filePart.setHeader(QNetworkRequest::ContentDispositionHeader, QStringLiteral("form-data; name=\"file\"; filename=\"%1\"").arg(info.fileName()));
0219     filePart.setBodyDevice(file);
0220     file->setParent(mp);
0221 
0222     mp->append(filePart);
0223 
0224     const auto uploadUrl = apiUrl(QStringLiteral("/api/v1/media"));
0225     qCDebug(TOKODON_HTTP) << "POST" << uploadUrl << "(upload)";
0226 
0227     return post(uploadUrl, mp, true, this, callback);
0228 }
0229 
0230 static QMap<QString, AbstractAccount::StreamingEventType> stringToStreamingEventType = {
0231     {QStringLiteral("update"), AbstractAccount::StreamingEventType::UpdateEvent},
0232     {QStringLiteral("delete"), AbstractAccount::StreamingEventType::DeleteEvent},
0233     {QStringLiteral("notification"), AbstractAccount::StreamingEventType::NotificationEvent},
0234     {QStringLiteral("filters_changed"), AbstractAccount::StreamingEventType::FiltersChangedEvent},
0235     {QStringLiteral("conversation"), AbstractAccount::StreamingEventType::ConversationEvent},
0236     {QStringLiteral("announcement"), AbstractAccount::StreamingEventType::AnnouncementEvent},
0237     {QStringLiteral("announcement.reaction"), AbstractAccount::StreamingEventType::AnnouncementRedactedEvent},
0238     {QStringLiteral("announcement.delete"), AbstractAccount::StreamingEventType::AnnouncementDeletedEvent},
0239     {QStringLiteral("status.update"), AbstractAccount::StreamingEventType::StatusUpdatedEvent},
0240     {QStringLiteral("encrypted_message"), AbstractAccount::StreamingEventType::EncryptedMessageChangedEvent},
0241 };
0242 
0243 QWebSocket *Account::streamingSocket(const QString &stream)
0244 {
0245     if (m_token.isEmpty()) {
0246         return nullptr;
0247     }
0248 
0249     if (m_websockets.contains(stream)) {
0250         return m_websockets[stream];
0251     }
0252 
0253     auto socket = new QWebSocket();
0254     socket->setParent(this);
0255 
0256     const auto url = streamingUrl(stream);
0257 
0258     connect(socket, &QWebSocket::textMessageReceived, this, [=](const QString &message) {
0259         const auto env = QJsonDocument::fromJson(message.toLocal8Bit());
0260         if (env.isObject() && env.object().contains("event"_L1)) {
0261             const auto event = stringToStreamingEventType[env.object()["event"_L1].toString()];
0262             Q_EMIT streamingEvent(event, env.object()["payload"_L1].toString().toLocal8Bit());
0263 
0264             if (event == NotificationEvent) {
0265                 const auto doc = QJsonDocument::fromJson(env.object()["payload"_L1].toString().toLocal8Bit());
0266                 handleNotification(doc);
0267                 return;
0268             }
0269         }
0270     });
0271 
0272     socket->open(url);
0273 
0274     m_websockets[stream] = socket;
0275     return socket;
0276 }
0277 
0278 void Account::validateToken(bool newAccount)
0279 {
0280     const QUrl verify_credentials = apiUrl(QStringLiteral("/api/v1/accounts/verify_credentials"));
0281 
0282     get(
0283         verify_credentials,
0284         true,
0285         this,
0286         [=](QNetworkReply *reply) {
0287             if (!reply->isFinished()) {
0288                 return;
0289             }
0290 
0291             const auto doc = QJsonDocument::fromJson(reply->readAll());
0292 
0293             if (!doc.isObject()) {
0294                 return;
0295             }
0296 
0297             const auto object = doc.object();
0298             if (!object.contains("source"_L1)) {
0299                 return;
0300             }
0301 
0302             m_identity = identityLookup(object["id"_L1].toString(), object);
0303             m_name = m_identity->username();
0304             Q_EMIT identityChanged();
0305             Q_EMIT authenticated(true, {});
0306 
0307 #ifdef HAVE_KUNIFIEDPUSH
0308             if (newAccount) {
0309                 // We asked for the push scope, so we can safely start subscribing to notifications
0310                 config()->setEnablePushNotifications(true);
0311                 config()->save();
0312             }
0313 #else
0314             Q_UNUSED(newAccount)
0315 #endif
0316 
0317 #ifdef HAVE_KUNIFIEDPUSH
0318             get(
0319                 apiUrl(QStringLiteral("/api/v1/push/subscription")),
0320                 true,
0321                 this,
0322                 [=](QNetworkReply *reply) {
0323                     m_hasPushSubscription = true;
0324 
0325                     const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
0326 
0327                     if (!NetworkController::instance().endpoint.isEmpty() && doc["endpoint"_L1] != NetworkController::instance().endpoint) {
0328                         qWarning(TOKODON_LOG) << "KUnifiedPush endpoint has changed to" << NetworkController::instance().endpoint << ", resubscribing!";
0329 
0330                         deleteResource(apiUrl(QStringLiteral("/api/v1/push/subscription")), true, this, [=](QNetworkReply *reply) {
0331                             Q_UNUSED(reply)
0332                             m_hasPushSubscription = false;
0333                             subscribePushNotifications();
0334                         });
0335                     } else {
0336                         updatePushNotifications();
0337                     }
0338                 },
0339                 [=](QNetworkReply *reply) {
0340                     Q_UNUSED(reply);
0341                     m_hasPushSubscription = false;
0342                     updatePushNotifications();
0343                 });
0344 #endif
0345         },
0346         [=](QNetworkReply *reply) {
0347             const auto doc = QJsonDocument::fromJson(reply->readAll());
0348 
0349             Q_EMIT authenticated(false, doc["error"_L1].toString());
0350         });
0351 
0352     fetchInstanceMetadata();
0353 
0354     // set up streaming for notifications
0355     streamingSocket(QStringLiteral("user"));
0356 }
0357 
0358 void Account::writeToSettings()
0359 {
0360     // do not write to settings if we do not have complete information yet,
0361     // or else it writes malformed and possibly duplicate accounts to settings.
0362     if (m_name.isEmpty() || m_instance_uri.isEmpty()) {
0363         return;
0364     }
0365 
0366     AccountConfig config(settingsGroupName());
0367     config.setClientId(m_client_id);
0368     config.setInstanceUri(m_instance_uri);
0369     config.setName(m_name);
0370     config.setIgnoreSslErrors(m_ignoreSslErrors);
0371 
0372     config.save();
0373 
0374     auto accessTokenJob = new QKeychain::WritePasswordJob{QStringLiteral("Tokodon"), this};
0375 #ifdef SAILFISHOS
0376     accessTokenJob->setInsecureFallback(true);
0377 #endif
0378     accessTokenJob->setKey(accessTokenKey());
0379     accessTokenJob->setTextData(m_token);
0380     accessTokenJob->start();
0381 
0382     auto clientSecretJob = new QKeychain::WritePasswordJob{QStringLiteral("Tokodon"), this};
0383 #ifdef SAILFISHOS
0384     clientSecretJob->setInsecureFallback(true);
0385 #endif
0386     clientSecretJob->setKey(clientSecretKey());
0387     clientSecretJob->setTextData(m_client_secret);
0388     clientSecretJob->start();
0389 }
0390 
0391 void Account::buildFromSettings()
0392 {
0393     m_client_id = m_config->clientId();
0394     m_name = m_config->name();
0395     m_instance_uri = m_config->instanceUri();
0396     m_ignoreSslErrors = m_config->ignoreSslErrors();
0397 
0398     auto accessTokenJob = new QKeychain::ReadPasswordJob{QStringLiteral("Tokodon"), this};
0399 #ifdef SAILFISHOS
0400     accessTokenJob->setInsecureFallback(true);
0401 #endif
0402     accessTokenJob->setKey(accessTokenKey());
0403 
0404     QObject::connect(accessTokenJob, &QKeychain::ReadPasswordJob::finished, [this, accessTokenJob]() {
0405         m_token = accessTokenJob->textData();
0406 
0407         validateToken();
0408     });
0409 
0410     accessTokenJob->start();
0411 
0412     auto clientSecretJob = new QKeychain::ReadPasswordJob{QStringLiteral("Tokodon"), this};
0413 #ifdef SAILFISHOS
0414     clientSecretJob->setInsecureFallback(true);
0415 #endif
0416     clientSecretJob->setKey(clientSecretKey());
0417 
0418     QObject::connect(clientSecretJob, &QKeychain::ReadPasswordJob::finished, [this, clientSecretJob]() {
0419         m_client_secret = clientSecretJob->textData();
0420     });
0421 
0422     clientSecretJob->start();
0423 }
0424 
0425 bool Account::hasFollowRequests() const
0426 {
0427     return m_hasFollowRequests;
0428 }
0429 
0430 void Account::checkForFollowRequests()
0431 {
0432     get(apiUrl(QStringLiteral("/api/v1/follow_requests")), true, this, [this](QNetworkReply *reply) {
0433         const auto followRequestResult = QJsonDocument::fromJson(reply->readAll());
0434         const bool hasFollowRequests = followRequestResult.isArray() && !followRequestResult.array().isEmpty();
0435         if (hasFollowRequests != m_hasFollowRequests) {
0436             m_hasFollowRequests = hasFollowRequests;
0437             Q_EMIT hasFollowRequestsChanged();
0438         }
0439     });
0440 }
0441 
0442 void Account::updatePushNotifications()
0443 {
0444 #ifdef HAVE_KUNIFIEDPUSH
0445     auto cfg = config();
0446 
0447     // If push notifications are explicitly disabled (like if we have an account that does not have the scope) skip
0448     if (!cfg->enablePushNotifications()) {
0449         return;
0450     }
0451 
0452     if (m_hasPushSubscription && !cfg->enableNotifications()) {
0453         unsubscribePushNotifications();
0454     } else if (!m_hasPushSubscription && cfg->enableNotifications()) {
0455         subscribePushNotifications();
0456     } else {
0457         QUrlQuery formdata = buildNotificationFormData();
0458 
0459         formdata.addQueryItem(QStringLiteral("policy"), QStringLiteral("all"));
0460 
0461         put(apiUrl(QStringLiteral("/api/v1/push/subscription")), formdata, true, this, [=](QNetworkReply *reply) {
0462             qCDebug(TOKODON_HTTP) << "Updated push notification rules:" << reply->readAll();
0463         });
0464     }
0465 #endif
0466 }
0467 
0468 void Account::unsubscribePushNotifications()
0469 {
0470 #ifdef HAVE_KUNIFIEDPUSH
0471     Q_ASSERT(m_hasPushSubscription);
0472     deleteResource(apiUrl(QStringLiteral("/api/v1/push/subscription")), true, this, [=](QNetworkReply *reply) {
0473         m_hasPushSubscription = false;
0474         qCDebug(TOKODON_HTTP) << "Unsubscribed from push notifications:" << reply->readAll();
0475     });
0476 #endif
0477 }
0478 
0479 void Account::subscribePushNotifications()
0480 {
0481 #ifdef HAVE_KUNIFIEDPUSH
0482     Q_ASSERT(!m_hasPushSubscription);
0483 
0484     // Generate 16 random bytes
0485     QByteArray randArray;
0486     for (int i = 0; i < 16; i++) {
0487         randArray.push_back(QRandomGenerator::global()->generate());
0488     }
0489 
0490     QUrlQuery formdata = buildNotificationFormData();
0491     formdata.addQueryItem(QStringLiteral("subscription[endpoint]"), QUrl(NetworkController::instance().endpoint).toString());
0492 
0493     // TODO: save this keypair in the keychain
0494     const auto keys = generateECDHKeypair();
0495 
0496     formdata.addQueryItem(QStringLiteral("subscription[keys][p256dh]"), QString::fromUtf8(exportPublicKey(keys).toBase64(QByteArray::Base64UrlEncoding)));
0497     formdata.addQueryItem(QStringLiteral("subscription[keys][auth]"), QString::fromUtf8(randArray.toBase64(QByteArray::Base64UrlEncoding)));
0498 
0499     formdata.addQueryItem(QStringLiteral("data[policy]"), QStringLiteral("all"));
0500 
0501     post(
0502         apiUrl(QStringLiteral("/api/v1/push/subscription")),
0503         formdata,
0504         true,
0505         this,
0506         [=](QNetworkReply *reply) {
0507             m_hasPushSubscription = true;
0508             qCDebug(TOKODON_HTTP) << "Subscribed to push notifications:" << reply->readAll();
0509         },
0510         [=](QNetworkReply *reply) {
0511             Q_UNUSED(reply); // to prevent a visible error
0512         });
0513 #endif
0514 }
0515 
0516 QUrlQuery Account::buildNotificationFormData()
0517 {
0518     auto cfg = config();
0519 
0520     QUrlQuery formdata;
0521     const auto addQuery = [&formdata](const QString key, const bool value) {
0522         formdata.addQueryItem(QStringLiteral("data[alerts][%1]").arg(key), value ? QStringLiteral("true") : QStringLiteral("false"));
0523     };
0524 
0525     addQuery(QStringLiteral("mention"), cfg->notifyMention());
0526     addQuery(QStringLiteral("status"), cfg->notifyStatus());
0527     addQuery(QStringLiteral("reblog"), cfg->notifyBoost());
0528     addQuery(QStringLiteral("follow"), cfg->notifyFollow());
0529     addQuery(QStringLiteral("follow_request"), cfg->notifyFollowRequest());
0530     addQuery(QStringLiteral("favourite"), cfg->notifyFavorite());
0531     addQuery(QStringLiteral("poll"), cfg->notifyPoll());
0532     addQuery(QStringLiteral("update"), cfg->notifyUpdate());
0533     addQuery(QStringLiteral("admin.sign_up"), cfg->notifySignup());
0534     addQuery(QStringLiteral("admin.report"), cfg->notifyReport());
0535 
0536     return formdata;
0537 }
0538 
0539 #include "moc_account.cpp"