File indexing completed on 2026-05-10 12:34: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 #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 }