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"