File indexing completed on 2024-05-12 16:28:03
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 "abstractaccount.h" 0006 0007 #include "accountmanager.h" 0008 #include "preferences.h" 0009 #include "relationship.h" 0010 #include "utils/messagefiltercontainer.h" 0011 #include "tokodon_debug.h" 0012 #include <KLocalizedString> 0013 #include <QFile> 0014 #include <QHttpMultiPart> 0015 #include <QNetworkReply> 0016 #include <QUrlQuery> 0017 0018 AbstractAccount::AbstractAccount(QObject *parent, const QString &instanceUri) 0019 : QObject(parent) 0020 , m_instance_uri(instanceUri) 0021 // default to 500, instances which support more signal it 0022 , m_maxPostLength(500) 0023 , m_maxPollOptions(4) 0024 , m_charactersReservedPerUrl(23) 0025 , m_identity(std::make_shared<Identity>()) 0026 , m_allowedContentTypes(AllowedContentType::PlainText) 0027 { 0028 } 0029 0030 AbstractAccount::AbstractAccount(QObject *parent) 0031 : QObject(parent) 0032 // default to 500, instances which support more signal it 0033 , m_maxPostLength(500) 0034 , m_maxPollOptions(4) 0035 , m_charactersReservedPerUrl(23) 0036 , m_identity(std::make_shared<Identity>()) 0037 , m_allowedContentTypes(AllowedContentType::PlainText) 0038 { 0039 } 0040 0041 Preferences *AbstractAccount::preferences() const 0042 { 0043 return m_preferences; 0044 } 0045 0046 QString AbstractAccount::username() const 0047 { 0048 return m_name; 0049 } 0050 0051 void AbstractAccount::setUsername(const QString &username) 0052 { 0053 if (m_name == username) { 0054 return; 0055 } 0056 m_name = username; 0057 Q_EMIT usernameChanged(); 0058 } 0059 0060 size_t AbstractAccount::maxPostLength() const 0061 { 0062 return m_maxPostLength; 0063 } 0064 0065 size_t AbstractAccount::maxPollOptions() const 0066 { 0067 return m_maxPollOptions; 0068 } 0069 0070 size_t AbstractAccount::charactersReservedPerUrl() const 0071 { 0072 return m_charactersReservedPerUrl; 0073 } 0074 0075 QString AbstractAccount::instanceName() const 0076 { 0077 return m_instance_name; 0078 } 0079 0080 bool AbstractAccount::haveToken() const 0081 { 0082 return !m_token.isEmpty(); 0083 } 0084 0085 bool AbstractAccount::hasName() const 0086 { 0087 return !m_name.isEmpty(); 0088 } 0089 0090 bool AbstractAccount::hasInstanceUrl() const 0091 { 0092 return !m_instance_uri.isEmpty(); 0093 } 0094 0095 QUrl AbstractAccount::apiUrl(const QString &path) const 0096 { 0097 QUrl url = QUrl::fromUserInput(m_instance_uri); 0098 url.setScheme("https"); 0099 url.setPath(path); 0100 0101 return url; 0102 } 0103 0104 void AbstractAccount::registerApplication(const QString &appName, const QString &website) 0105 { 0106 // clear any previous bearer token credentials 0107 m_token = QString(); 0108 0109 // register 0110 const QUrl regUrl = apiUrl("/api/v1/apps"); 0111 const QJsonObject obj = { 0112 {"client_name", appName}, 0113 {"redirect_uris", "urn:ietf:wg:oauth:2.0:oob"}, 0114 {"scopes", "read write follow admin:read admin:write"}, 0115 {"website", website}, 0116 }; 0117 const QJsonDocument doc(obj); 0118 0119 post(regUrl, doc, false, this, [=](QNetworkReply *reply) { 0120 if (!reply->isFinished()) { 0121 qCDebug(TOKODON_LOG) << "not finished"; 0122 return; 0123 } 0124 0125 const auto data = reply->readAll(); 0126 const auto doc = QJsonDocument::fromJson(data); 0127 0128 m_client_id = doc.object()["client_id"].toString(); 0129 m_client_secret = doc.object()["client_secret"].toString(); 0130 0131 s_messageFilter->insert(m_client_secret, "CLIENT_SECRET"); 0132 0133 if (isRegistered()) { 0134 Q_EMIT registered(); 0135 } 0136 }); 0137 } 0138 0139 Identity *AbstractAccount::identity() 0140 { 0141 return m_identity.get(); 0142 } 0143 0144 std::shared_ptr<Identity> AbstractAccount::identityLookup(const QString &accountId, const QJsonObject &doc) 0145 { 0146 if (m_identity && m_identity->id() == accountId) { 0147 return m_identity; 0148 } 0149 auto id = m_identityCache[accountId]; 0150 if (id && id->id() == accountId) { 0151 return id; 0152 } 0153 0154 id = std::make_shared<Identity>(); 0155 id->reparentIdentity(this); 0156 id->fromSourceData(doc); 0157 m_identityCache[accountId] = id; 0158 0159 return m_identityCache[accountId]; 0160 } 0161 0162 std::shared_ptr<AdminAccountInfo> AbstractAccount::adminIdentityLookup(const QString &accountId, const QJsonObject &doc) 0163 { 0164 if (m_adminIdentity && m_adminIdentity->userLevelIdentity()->id() == accountId) { 0165 return m_adminIdentity; 0166 } 0167 auto id = m_adminIdentityCache[accountId]; 0168 if (id && id->userLevelIdentity()->id() == accountId) { 0169 return id; 0170 } 0171 0172 id = std::make_shared<AdminAccountInfo>(); 0173 id->reparentAdminAccountInfo(this); 0174 id->fromSourceData(doc); 0175 m_adminIdentityCache[accountId] = id; 0176 0177 return m_adminIdentityCache[accountId]; 0178 } 0179 0180 bool AbstractAccount::identityCached(const QString &accountId) const 0181 { 0182 if (m_identity && m_identity->id() == accountId) { 0183 return true; 0184 } 0185 auto id = m_identityCache[accountId]; 0186 return id && id->id() == accountId; 0187 } 0188 0189 QUrlQuery AbstractAccount::buildOAuthQuery() const 0190 { 0191 return QUrlQuery{{"client_id", m_client_id}}; 0192 } 0193 0194 QUrl AbstractAccount::getAuthorizeUrl() const 0195 { 0196 QUrl url = apiUrl("/oauth/authorize"); 0197 QUrlQuery q = buildOAuthQuery(); 0198 0199 q.addQueryItem("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"); 0200 q.addQueryItem("response_type", "code"); 0201 q.addQueryItem("scope", "read write follow admin:read admin:write"); 0202 0203 url.setQuery(q); 0204 0205 return url; 0206 } 0207 0208 QUrl AbstractAccount::getTokenUrl() const 0209 { 0210 return apiUrl("/oauth/token"); 0211 } 0212 0213 void AbstractAccount::setInstanceUri(const QString &instance_uri) 0214 { 0215 // instance URI changed, get new credentials 0216 QUrl instance_url = QUrl::fromUserInput(instance_uri); 0217 instance_url.setScheme("https"); // getting token from http is not supported 0218 0219 m_instance_uri = instance_url.toString(); 0220 registerApplication("Tokodon", "https://apps.kde.org/tokodon"); 0221 } 0222 0223 QString AbstractAccount::instanceUri() const 0224 { 0225 return m_instance_uri; 0226 } 0227 0228 void AbstractAccount::setToken(const QString &authcode) 0229 { 0230 const QUrl tokenUrl = getTokenUrl(); 0231 QUrlQuery q = buildOAuthQuery(); 0232 0233 q.addQueryItem("client_secret", m_client_secret); 0234 q.addQueryItem("redirect_uri", "urn:ietf:wg:oauth:2.0:oob"); 0235 q.addQueryItem("grant_type", "authorization_code"); 0236 q.addQueryItem("code", authcode); 0237 0238 post(tokenUrl, q, false, this, [=](QNetworkReply *reply) { 0239 auto data = reply->readAll(); 0240 auto doc = QJsonDocument::fromJson(data); 0241 0242 m_token = doc.object()["access_token"].toString(); 0243 s_messageFilter->insert(m_token, "ACCESS_TOKEN"); 0244 AccountManager::instance().addAccount(this); 0245 AccountManager::instance().selectAccount(this, true); 0246 validateToken(); 0247 }); 0248 } 0249 0250 void AbstractAccount::mutatePost(Post *p, const QString &verb, bool deliver_home) 0251 { 0252 const QUrl mutation_url = apiUrl(QString("/api/v1/statuses/%1/%2").arg(p->postId(), verb)); 0253 const QJsonDocument doc; 0254 0255 post(mutation_url, doc, true, this, [=](QNetworkReply *reply) { 0256 const auto data = reply->readAll(); 0257 const auto doc = QJsonDocument::fromJson(data); 0258 0259 if (deliver_home) { 0260 QList<Post *> posts; 0261 auto obj = doc.object(); 0262 0263 auto p = new Post(this, obj, this); 0264 posts.push_back(p); 0265 0266 Q_EMIT fetchedTimeline("home", posts); 0267 } 0268 }); 0269 } 0270 0271 void AbstractAccount::favorite(Post *p) 0272 { 0273 mutatePost(p, "favourite"); 0274 } 0275 0276 void AbstractAccount::unfavorite(Post *p) 0277 { 0278 mutatePost(p, "unfavourite"); 0279 } 0280 0281 void AbstractAccount::repeat(Post *p) 0282 { 0283 mutatePost(p, "reblog", true); 0284 } 0285 0286 void AbstractAccount::unrepeat(Post *p) 0287 { 0288 mutatePost(p, "unreblog"); 0289 } 0290 0291 void AbstractAccount::bookmark(Post *p) 0292 { 0293 mutatePost(p, "bookmark", true); 0294 } 0295 0296 void AbstractAccount::unbookmark(Post *p) 0297 { 0298 mutatePost(p, "unbookmark"); 0299 } 0300 0301 void AbstractAccount::pin(Post *p) 0302 { 0303 mutatePost(p, "pin", true); 0304 } 0305 0306 void AbstractAccount::unpin(Post *p) 0307 { 0308 mutatePost(p, "unpin"); 0309 } 0310 0311 // It seemed clearer to keep this logic separate from the general instance metadata collection, on the off chance 0312 // that it might need to be extended later on. 0313 static AbstractAccount::AllowedContentType parseVersion(const QString &instanceVer) 0314 { 0315 using ContentType = AbstractAccount::AllowedContentType; 0316 0317 unsigned int result = ContentType::PlainText; 0318 if (instanceVer.contains("glitch")) { 0319 result |= ContentType::Markdown | ContentType::Html; 0320 } 0321 0322 return static_cast<ContentType>(result); 0323 } 0324 0325 static QMap<QString, AbstractAccount::AllowedContentType> stringToContentType = { 0326 {"text/plain", AbstractAccount::AllowedContentType::PlainText}, 0327 {"text/bbcode", AbstractAccount::AllowedContentType::BBCode}, 0328 {"text/html", AbstractAccount::AllowedContentType::Html}, 0329 {"text/markdown", AbstractAccount::AllowedContentType::Markdown}, 0330 }; 0331 0332 static AbstractAccount::AllowedContentType parsePleromaInfo(const QJsonDocument &doc) 0333 { 0334 using ContentType = AbstractAccount::AllowedContentType; 0335 unsigned int result = ContentType::PlainText; 0336 0337 auto obj = doc.object(); 0338 if (obj.contains("metadata")) { 0339 auto metadata = obj["metadata"].toObject(); 0340 if (!metadata.contains("postFormats")) 0341 return static_cast<ContentType>(result); 0342 0343 auto formats = metadata["postFormats"].toArray(); 0344 0345 for (auto c : formats) { 0346 auto fmt = c.toString(); 0347 0348 if (!stringToContentType.contains(fmt)) 0349 continue; 0350 0351 result |= (unsigned int)stringToContentType[fmt]; 0352 } 0353 } 0354 0355 return static_cast<ContentType>(result); 0356 } 0357 0358 void AbstractAccount::fetchInstanceMetadata() 0359 { 0360 QUrl instance_url = apiUrl("/api/v1/instance"); 0361 QUrl pleroma_info = apiUrl("/nodeinfo/2.1.json"); 0362 0363 get(instance_url, false, this, [=](QNetworkReply *reply) { 0364 if (200 != reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)) 0365 return; 0366 0367 const auto data = reply->readAll(); 0368 const auto doc = QJsonDocument::fromJson(data); 0369 0370 if (!doc.isObject()) 0371 return; 0372 0373 const auto obj = doc.object(); 0374 0375 if (obj.contains("configuration")) { 0376 const auto configObj = obj["configuration"].toObject(); 0377 0378 if (configObj.contains("statuses")) { 0379 const auto statusConfigObj = configObj["statuses"].toObject(); 0380 m_maxPostLength = statusConfigObj["max_characters"].toInt(); 0381 m_charactersReservedPerUrl = statusConfigObj["characters_reserved_per_url"].toInt(); 0382 } 0383 } 0384 0385 // One can only hope that there will always be a version attached 0386 if (obj.contains("version")) { 0387 m_allowedContentTypes = parseVersion(obj["version"].toString()); 0388 } 0389 0390 // Pleroma/Akkoma may report maximum post characters here, instead 0391 if (obj.contains("max_toot_chars")) { 0392 m_maxPostLength = obj["max_toot_chars"].toInt(); 0393 } 0394 0395 // Pleroma/Akkoma can report higher poll limits 0396 if (obj.contains("poll_limits")) { 0397 m_maxPollOptions = obj["poll_limits"].toObject()["max_options"].toInt(); 0398 } 0399 0400 m_instance_name = obj["title"].toString(); 0401 Q_EMIT fetchedInstanceMetadata(); 0402 }); 0403 m_instance_name = QString("social"); 0404 Q_EMIT fetchedInstanceMetadata(); 0405 0406 get(pleroma_info, false, this, [=](QNetworkReply *reply) { 0407 const auto data = reply->readAll(); 0408 const auto doc = QJsonDocument::fromJson(data); 0409 0410 m_allowedContentTypes = parsePleromaInfo(doc); 0411 Q_EMIT fetchedInstanceMetadata(); 0412 }); 0413 } 0414 0415 void AbstractAccount::invalidate() 0416 { 0417 Q_EMIT invalidated(); 0418 } 0419 0420 void AbstractAccount::invalidatePost(Post *p) 0421 { 0422 Q_EMIT invalidatedPost(p); 0423 } 0424 0425 QUrl AbstractAccount::streamingUrl(const QString &stream) 0426 { 0427 QUrl url = apiUrl("/api/v1/streaming"); 0428 url.setQuery(QUrlQuery{ 0429 {"access_token", m_token}, 0430 {"stream", stream}, 0431 }); 0432 url.setScheme("wss"); 0433 0434 return url; 0435 } 0436 0437 void AbstractAccount::handleNotification(const QJsonDocument &doc) 0438 { 0439 const auto obj = doc.object(); 0440 std::shared_ptr<Notification> n = std::make_shared<Notification>(this, obj); 0441 0442 Q_EMIT notification(n); 0443 } 0444 0445 void AbstractAccount::executeAction(Identity *identity, AccountAction accountAction, const QJsonObject &extraArguments) 0446 { 0447 const QHash<AccountAction, QString> accountActionMap = { 0448 {AccountAction::Follow, QStringLiteral("/follow")}, 0449 {AccountAction::Unfollow, QStringLiteral("/unfollow")}, 0450 {AccountAction::Block, QStringLiteral("/block")}, 0451 {AccountAction::Unblock, QStringLiteral("/unblock")}, 0452 {AccountAction::Mute, QStringLiteral("/mute")}, 0453 {AccountAction::Unmute, QStringLiteral("/unmute")}, 0454 {AccountAction::Feature, QStringLiteral("/pin")}, 0455 {AccountAction::Unfeature, QStringLiteral("/unpin")}, 0456 {AccountAction::Note, QStringLiteral("/note")}, 0457 }; 0458 0459 const auto apiCall = accountActionMap[accountAction]; 0460 0461 const auto accountId = identity->id(); 0462 const QString accountApiUrl = QStringLiteral("/api/v1/accounts/") + accountId + apiCall; 0463 const QJsonDocument doc(extraArguments); 0464 0465 post(apiUrl(accountApiUrl), doc, true, this, [=](QNetworkReply *reply) { 0466 auto doc = QJsonDocument::fromJson(reply->readAll()); 0467 auto jsonObj = doc.object(); 0468 0469 // Check if the request failed due to one account blocking the other 0470 if (!jsonObj.value("error").isUndefined()) { 0471 const QHash<AccountAction, QString> accountActionMap = { 0472 {AccountAction::Follow, i18n("Could not follow account")}, 0473 {AccountAction::Unfollow, i18n("Could not unfollow account")}, 0474 {AccountAction::Block, i18n("Could not block account")}, 0475 {AccountAction::Unblock, i18n("Could not unblock account")}, 0476 {AccountAction::Mute, i18n("Could not mute account")}, 0477 {AccountAction::Unmute, i18n("Could not unmute account")}, 0478 {AccountAction::Feature, i18n("Could not feature account")}, 0479 {AccountAction::Unfeature, i18n("Could not unfeature account")}, 0480 {AccountAction::Note, i18n("Could not edit note about an account")}, 0481 }; 0482 const auto errorMessage = accountActionMap[accountAction]; 0483 Q_EMIT errorOccured(errorMessage); 0484 return; 0485 } 0486 // If returned json obj is not an error, it's a relationship status. 0487 // Returned relationship should have a value of true 0488 // under either the "following" or "requested" keys. 0489 auto relationship = identity->relationship(); 0490 relationship->updateFromJson(jsonObj); 0491 0492 Q_EMIT identity->relationshipChanged(); 0493 }); 0494 } 0495 0496 void AbstractAccount::followAccount(Identity *identity, bool reblogs, bool notify) 0497 { 0498 executeAction(identity, 0499 AccountAction::Follow, 0500 { 0501 {"reblogs", reblogs}, 0502 {"notify", notify}, 0503 }); 0504 } 0505 0506 void AbstractAccount::unfollowAccount(Identity *identity) 0507 { 0508 executeAction(identity, AccountAction::Unfollow); 0509 } 0510 0511 void AbstractAccount::blockAccount(Identity *identity) 0512 { 0513 executeAction(identity, AccountAction::Block); 0514 } 0515 0516 void AbstractAccount::unblockAccount(Identity *identity) 0517 { 0518 executeAction(identity, AccountAction::Unblock); 0519 } 0520 0521 void AbstractAccount::muteAccount(Identity *identity, bool notifications, int duration) 0522 { 0523 executeAction(identity, AccountAction::Mute, {{"notifcations", notifications}, {"duration", duration}}); 0524 } 0525 0526 void AbstractAccount::unmuteAccount(Identity *identity) 0527 { 0528 executeAction(identity, AccountAction::Unmute); 0529 } 0530 0531 void AbstractAccount::featureAccount(Identity *identity) 0532 { 0533 executeAction(identity, AccountAction::Feature); 0534 } 0535 0536 void AbstractAccount::unfeatureAccount(Identity *identity) 0537 { 0538 executeAction(identity, AccountAction::Unfeature); 0539 } 0540 0541 void AbstractAccount::addNote(Identity *identity, const QString ¬e) 0542 { 0543 if (note.isEmpty()) { 0544 executeAction(identity, AccountAction::Note); 0545 } else { 0546 executeAction(identity, AccountAction::Note, {{"comment", note}}); 0547 } 0548 } 0549 0550 bool AbstractAccount::isRegistered() const 0551 { 0552 return !m_client_id.isEmpty() && !m_client_secret.isEmpty(); 0553 } 0554 0555 QString AbstractAccount::settingsGroupName() const 0556 { 0557 return AccountManager::settingsGroupName(m_name, m_instance_uri); 0558 } 0559 0560 QString AbstractAccount::clientSecretKey() const 0561 { 0562 return AccountManager::clientSecretKey(settingsGroupName()); 0563 } 0564 0565 QString AbstractAccount::accessTokenKey() const 0566 { 0567 return AccountManager::accessTokenKey(settingsGroupName()); 0568 }