File indexing completed on 2024-05-12 05:04:07
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 "messagefiltercontainer.h" 0009 #include "navigation.h" 0010 #include "networkcontroller.h" 0011 #include "relationship.h" 0012 #include "tokodon_debug.h" 0013 0014 #include <KLocalizedString> 0015 0016 using namespace Qt::Literals::StringLiterals; 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_supportsLocalVisibility(false) 0025 , m_charactersReservedPerUrl(23) 0026 , m_identity(std::make_shared<Identity>()) 0027 , m_allowedContentTypes(AllowedContentType::PlainText) 0028 { 0029 } 0030 0031 AbstractAccount::AbstractAccount(QObject *parent) 0032 : QObject(parent) 0033 // default to 500, instances which support more signal it 0034 , m_maxPostLength(500) 0035 , m_maxPollOptions(4) 0036 , m_supportsLocalVisibility(false) 0037 , m_charactersReservedPerUrl(23) 0038 , m_identity(std::make_shared<Identity>()) 0039 , m_allowedContentTypes(AllowedContentType::PlainText) 0040 { 0041 } 0042 0043 AccountConfig *AbstractAccount::config() 0044 { 0045 if (!m_config) { 0046 m_config = new AccountConfig{settingsGroupName(), this}; 0047 } 0048 Q_ASSERT(m_config != nullptr); 0049 return m_config; 0050 } 0051 0052 Preferences *AbstractAccount::preferences() const 0053 { 0054 return m_preferences; 0055 } 0056 0057 QString AbstractAccount::username() const 0058 { 0059 return m_name; 0060 } 0061 0062 void AbstractAccount::setUsername(const QString &username) 0063 { 0064 if (m_name == username) { 0065 return; 0066 } 0067 m_name = username; 0068 Q_EMIT usernameChanged(); 0069 } 0070 0071 size_t AbstractAccount::maxPostLength() const 0072 { 0073 return m_maxPostLength; 0074 } 0075 0076 size_t AbstractAccount::maxPollOptions() const 0077 { 0078 return m_maxPollOptions; 0079 } 0080 0081 bool AbstractAccount::supportsLocalVisibility() const 0082 { 0083 return m_supportsLocalVisibility; 0084 } 0085 0086 size_t AbstractAccount::charactersReservedPerUrl() const 0087 { 0088 return m_charactersReservedPerUrl; 0089 } 0090 0091 QString AbstractAccount::instanceName() const 0092 { 0093 return m_instance_name; 0094 } 0095 0096 bool AbstractAccount::haveToken() const 0097 { 0098 return !m_token.isEmpty(); 0099 } 0100 0101 bool AbstractAccount::hasName() const 0102 { 0103 return !m_name.isEmpty(); 0104 } 0105 0106 bool AbstractAccount::hasInstanceUrl() const 0107 { 0108 return !m_instance_uri.isEmpty(); 0109 } 0110 0111 QUrl AbstractAccount::apiUrl(const QString &path) const 0112 { 0113 QUrl url = QUrl::fromUserInput(m_instance_uri); 0114 url.setScheme(QStringLiteral("https")); 0115 url.setPath(path); 0116 0117 return url; 0118 } 0119 0120 void AbstractAccount::registerApplication(const QString &appName, const QString &website, const QString &additionalScopes) 0121 { 0122 // clear any previous bearer token credentials 0123 m_token = QString(); 0124 0125 QString ourAdditionalScopes; 0126 #ifdef HAVE_KUNIFIEDPUSH 0127 ourAdditionalScopes = QStringLiteral("push "); 0128 #endif 0129 0130 // Store for future usage (e.g. authorizeUrl) 0131 m_additionalScopes = ourAdditionalScopes + additionalScopes; 0132 0133 // register 0134 const QUrl regUrl = apiUrl(QStringLiteral("/api/v1/apps")); 0135 const QJsonObject obj{ 0136 {QStringLiteral("client_name"), appName}, 0137 {QStringLiteral("redirect_uris"), QStringLiteral("tokodon://oauth")}, 0138 {QStringLiteral("scopes"), QStringLiteral("read write follow %1").arg(m_additionalScopes)}, 0139 {QStringLiteral("website"), website}, 0140 }; 0141 const QJsonDocument doc(obj); 0142 0143 post(regUrl, doc, false, this, [=](QNetworkReply *reply) { 0144 if (!reply->isFinished()) { 0145 qCDebug(TOKODON_LOG) << "not finished"; 0146 return; 0147 } 0148 0149 const auto data = reply->readAll(); 0150 const auto doc = QJsonDocument::fromJson(data); 0151 0152 m_client_id = doc.object()["client_id"_L1].toString(); 0153 m_client_secret = doc.object()["client_secret"_L1].toString(); 0154 0155 s_messageFilter->insert(m_client_secret, QStringLiteral("CLIENT_SECRET")); 0156 0157 if (isRegistered()) { 0158 Q_EMIT registered(); 0159 fetchInstanceMetadata(); 0160 } 0161 }); 0162 } 0163 0164 void AbstractAccount::registerAccount(const QString &username, 0165 const QString &email, 0166 const QString &password, 0167 bool agreement, 0168 const QString &locale, 0169 const QString &reason) 0170 { 0171 // get an app-level access token, obviously we don't have a user token yet. 0172 const QUrl tokenUrl = getTokenUrl(); 0173 QUrlQuery q = buildOAuthQuery(); 0174 0175 q.addQueryItem(QStringLiteral("client_secret"), m_client_secret); 0176 q.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("client_credentials")); 0177 q.addQueryItem(QStringLiteral("scope"), QStringLiteral("write")); 0178 0179 post(tokenUrl, q, false, this, [=](QNetworkReply *reply) { 0180 auto data = reply->readAll(); 0181 auto doc = QJsonDocument::fromJson(data); 0182 0183 // override the token for now 0184 m_token = doc.object()["access_token"_L1].toString(); 0185 s_messageFilter->insert(m_token, QStringLiteral("ACCESS_TOKEN")); 0186 0187 const QUrlQuery formdata{ 0188 {QStringLiteral("username"), username}, 0189 {QStringLiteral("email"), email}, 0190 {QStringLiteral("password"), password}, 0191 {QStringLiteral("agreement"), agreement ? QStringLiteral("1") : QStringLiteral("0")}, 0192 {QStringLiteral("locale"), locale}, 0193 {QStringLiteral("reason"), reason}, 0194 }; 0195 0196 post( 0197 apiUrl(QStringLiteral("/api/v1/accounts")), 0198 formdata, 0199 true, 0200 this, 0201 [=](QNetworkReply *reply) { 0202 const auto data = reply->readAll(); 0203 const auto doc = QJsonDocument::fromJson(data); 0204 0205 if (doc.object().contains("access_token"_L1)) { 0206 setUsername(username); 0207 setAccessToken(doc["access_token"_L1].toString()); 0208 } 0209 }, 0210 [=](QNetworkReply *reply) { 0211 Q_EMIT registrationError(QString::fromUtf8(reply->readAll())); 0212 }); 0213 }); 0214 } 0215 0216 Identity *AbstractAccount::identity() 0217 { 0218 return m_identity.get(); 0219 } 0220 0221 std::shared_ptr<Identity> AbstractAccount::identityLookup(const QString &accountId, const QJsonObject &doc) 0222 { 0223 if (m_identity && m_identity->id() == accountId) { 0224 return m_identity; 0225 } 0226 auto id = m_identityCache[accountId]; 0227 if (id && id->id() == accountId) { 0228 return id; 0229 } 0230 0231 id = std::make_shared<Identity>(); 0232 id->reparentIdentity(this); 0233 id->fromSourceData(doc); 0234 m_identityCache[accountId] = id; 0235 0236 return m_identityCache[accountId]; 0237 } 0238 0239 std::shared_ptr<AdminAccountInfo> AbstractAccount::adminIdentityLookup(const QString &accountId, const QJsonObject &doc) 0240 { 0241 if (m_adminIdentity && m_adminIdentity->userLevelIdentity()->id() == accountId) { 0242 return m_adminIdentity; 0243 } 0244 auto id = m_adminIdentityCache[accountId]; 0245 if (id && id->userLevelIdentity()->id() == accountId) { 0246 return id; 0247 } 0248 0249 id = std::make_shared<AdminAccountInfo>(); 0250 id->reparentAdminAccountInfo(this); 0251 id->fromSourceData(doc); 0252 m_adminIdentityCache[accountId] = id; 0253 0254 return m_adminIdentityCache[accountId]; 0255 } 0256 0257 AdminAccountInfo *AbstractAccount::adminIdentityLookupWithVanillaPointer(const QString &accountId, const QJsonObject &doc) 0258 { 0259 const auto id = new AdminAccountInfo(); 0260 0261 id->reparentAdminAccountInfo(this); 0262 id->fromSourceData(doc); 0263 m_adminIdentityCacheWithVanillaPointer[accountId] = id; 0264 0265 return m_adminIdentityCacheWithVanillaPointer[accountId]; 0266 } 0267 0268 std::shared_ptr<ReportInfo> AbstractAccount::reportInfoLookup(const QString &reportId, const QJsonObject &doc) 0269 { 0270 if (m_reportInfo && m_reportInfo->reportId() == reportId) { 0271 return m_reportInfo; 0272 } 0273 auto id = m_reportInfoCache[reportId]; 0274 if (id && id->reportId() == reportId) { 0275 return id; 0276 } 0277 0278 id = std::make_shared<ReportInfo>(); 0279 id->reparentReportInfo(this); 0280 id->fromSourceData(doc); 0281 m_reportInfoCache[reportId] = id; 0282 0283 return m_reportInfoCache[reportId]; 0284 } 0285 0286 bool AbstractAccount::identityCached(const QString &accountId) const 0287 { 0288 if (m_identity && m_identity->id() == accountId) { 0289 return true; 0290 } 0291 auto id = m_identityCache[accountId]; 0292 return id && id->id() == accountId; 0293 } 0294 0295 QUrlQuery AbstractAccount::buildOAuthQuery() const 0296 { 0297 return QUrlQuery{{QStringLiteral("client_id"), m_client_id}}; 0298 } 0299 0300 QUrl AbstractAccount::getAuthorizeUrl() const 0301 { 0302 QUrl url = apiUrl(QStringLiteral("/oauth/authorize")); 0303 QUrlQuery q = buildOAuthQuery(); 0304 0305 q.addQueryItem(QStringLiteral("redirect_uri"), QStringLiteral("tokodon://oauth")); 0306 q.addQueryItem(QStringLiteral("response_type"), QStringLiteral("code")); 0307 q.addQueryItem(QStringLiteral("scope"), QStringLiteral("read write follow ") + m_additionalScopes); 0308 0309 url.setQuery(q); 0310 0311 return url; 0312 } 0313 0314 void AbstractAccount::setAccessToken(const QString &token) 0315 { 0316 m_token = token; 0317 s_messageFilter->insert(m_token, QStringLiteral("ACCESS_TOKEN")); 0318 AccountManager::instance().addAccount(this, false); 0319 AccountManager::instance().selectAccount(this, true); 0320 validateToken(true); 0321 } 0322 0323 QUrl AbstractAccount::getTokenUrl() const 0324 { 0325 return apiUrl(QStringLiteral("/oauth/token")); 0326 } 0327 0328 void AbstractAccount::setInstanceUri(const QString &instance_uri) 0329 { 0330 // instance URI changed, get new credentials 0331 QUrl instance_url = QUrl::fromUserInput(instance_uri); 0332 instance_url.setScheme(QStringLiteral("https")); // getting token from http is not supported 0333 0334 m_instance_uri = instance_url.toString(); 0335 } 0336 0337 QString AbstractAccount::instanceUri() const 0338 { 0339 return m_instance_uri; 0340 } 0341 0342 void AbstractAccount::setToken(const QString &authcode) 0343 { 0344 const QUrl tokenUrl = getTokenUrl(); 0345 QUrlQuery q = buildOAuthQuery(); 0346 0347 q.addQueryItem(QStringLiteral("client_secret"), m_client_secret); 0348 q.addQueryItem(QStringLiteral("redirect_uri"), QStringLiteral("tokodon://oauth")); 0349 q.addQueryItem(QStringLiteral("grant_type"), QStringLiteral("authorization_code")); 0350 q.addQueryItem(QStringLiteral("code"), authcode); 0351 0352 post(tokenUrl, q, false, this, [=](QNetworkReply *reply) { 0353 auto data = reply->readAll(); 0354 auto doc = QJsonDocument::fromJson(data); 0355 0356 setAccessToken(doc.object()["access_token"_L1].toString()); 0357 }); 0358 } 0359 0360 void AbstractAccount::mutatePost(const QString &id, const QString &verb, bool deliver_home) 0361 { 0362 const QUrl mutation_url = apiUrl(QStringLiteral("/api/v1/statuses/%1/%2").arg(id, verb)); 0363 const QJsonDocument doc; 0364 0365 post(mutation_url, doc, true, this, [=](QNetworkReply *reply) { 0366 const auto data = reply->readAll(); 0367 const auto doc = QJsonDocument::fromJson(data); 0368 0369 if (deliver_home) { 0370 QList<Post *> posts; 0371 auto obj = doc.object(); 0372 0373 auto p = new Post(this, obj, this); 0374 posts.push_back(p); 0375 0376 Q_EMIT fetchedTimeline(QStringLiteral("home"), posts); 0377 } 0378 }); 0379 } 0380 0381 void AbstractAccount::favorite(Post *p) 0382 { 0383 mutatePost(p->postId(), QStringLiteral("favourite")); 0384 } 0385 0386 void AbstractAccount::unfavorite(Post *p) 0387 { 0388 mutatePost(p->postId(), QStringLiteral("unfavourite")); 0389 } 0390 0391 void AbstractAccount::repeat(Post *p) 0392 { 0393 mutatePost(p->postId(), QStringLiteral("reblog"), true); 0394 } 0395 0396 void AbstractAccount::unrepeat(Post *p) 0397 { 0398 mutatePost(p->postId(), QStringLiteral("unreblog")); 0399 } 0400 0401 void AbstractAccount::bookmark(Post *p) 0402 { 0403 mutatePost(p->postId(), QStringLiteral("bookmark"), true); 0404 } 0405 0406 void AbstractAccount::unbookmark(Post *p) 0407 { 0408 mutatePost(p->postId(), QStringLiteral("unbookmark")); 0409 } 0410 0411 void AbstractAccount::pin(Post *p) 0412 { 0413 mutatePost(p->postId(), QStringLiteral("pin"), true); 0414 } 0415 0416 void AbstractAccount::unpin(Post *p) 0417 { 0418 mutatePost(p->postId(), QStringLiteral("unpin")); 0419 } 0420 0421 // It seemed clearer to keep this logic separate from the general instance metadata collection, on the off chance 0422 // that it might need to be extended later on. 0423 static AbstractAccount::AllowedContentType parseVersion(const QString &instanceVer) 0424 { 0425 using ContentType = AbstractAccount::AllowedContentType; 0426 0427 unsigned int result = ContentType::PlainText; 0428 if (instanceVer.contains("glitch"_L1)) { 0429 result |= ContentType::Markdown | ContentType::Html; 0430 } 0431 0432 return static_cast<ContentType>(result); 0433 } 0434 0435 static QMap<QString, AbstractAccount::AllowedContentType> stringToContentType = { 0436 {QStringLiteral("text/plain"), AbstractAccount::AllowedContentType::PlainText}, 0437 {QStringLiteral("text/bbcode"), AbstractAccount::AllowedContentType::BBCode}, 0438 {QStringLiteral("text/html"), AbstractAccount::AllowedContentType::Html}, 0439 {QStringLiteral("text/markdown"), AbstractAccount::AllowedContentType::Markdown}, 0440 }; 0441 0442 static AbstractAccount::AllowedContentType parsePleromaInfo(const QJsonDocument &doc) 0443 { 0444 using ContentType = AbstractAccount::AllowedContentType; 0445 unsigned int result = ContentType::PlainText; 0446 0447 auto obj = doc.object(); 0448 if (obj.contains("metadata"_L1)) { 0449 auto metadata = obj["metadata"_L1].toObject(); 0450 if (!metadata.contains("postFormats"_L1)) 0451 return static_cast<ContentType>(result); 0452 0453 auto formats = metadata["postFormats"_L1].toArray(); 0454 0455 for (auto c : formats) { 0456 auto fmt = c.toString(); 0457 0458 if (!stringToContentType.contains(fmt)) 0459 continue; 0460 0461 result |= (unsigned int)stringToContentType[fmt]; 0462 } 0463 } 0464 0465 return static_cast<ContentType>(result); 0466 } 0467 0468 void AbstractAccount::fetchInstanceMetadata() 0469 { 0470 get( 0471 apiUrl(QStringLiteral("/api/v2/instance")), 0472 false, 0473 this, 0474 [=](QNetworkReply *reply) { 0475 if (200 != reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)) 0476 return; 0477 0478 const auto data = reply->readAll(); 0479 const auto doc = QJsonDocument::fromJson(data); 0480 0481 if (!doc.isObject()) 0482 return; 0483 0484 const auto obj = doc.object(); 0485 0486 if (obj.contains("configuration"_L1)) { 0487 const auto configObj = obj["configuration"_L1].toObject(); 0488 0489 if (configObj.contains("statuses"_L1)) { 0490 const auto statusConfigObj = configObj["statuses"_L1].toObject(); 0491 m_maxPostLength = statusConfigObj["max_characters"_L1].toInt(); 0492 m_charactersReservedPerUrl = statusConfigObj["characters_reserved_per_url"_L1].toInt(); 0493 } 0494 } 0495 0496 // One can only hope that there will always be a version attached 0497 if (obj.contains("version"_L1)) { 0498 m_allowedContentTypes = parseVersion(obj["version"_L1].toString()); 0499 } 0500 0501 // Pleroma/Akkoma may report maximum post characters here, instead 0502 if (obj.contains("max_toot_chars"_L1)) { 0503 m_maxPostLength = obj["max_toot_chars"_L1].toInt(); 0504 } 0505 0506 // Pleroma/Akkoma can report higher poll limits 0507 if (obj.contains("poll_limits"_L1)) { 0508 m_maxPollOptions = obj["poll_limits"_L1].toObject()["max_options"_L1].toInt(); 0509 } 0510 0511 // Other instance of poll options 0512 if (obj.contains("polls"_L1)) { 0513 m_maxPollOptions = obj["polls"_L1].toObject()["max_options"_L1].toInt(); 0514 } 0515 0516 if (obj.contains("registrations"_L1)) { 0517 m_registrationsOpen = obj["registrations"_L1].toObject()["enabled"_L1].toBool(); 0518 m_registrationMessage = obj["registrations"_L1].toObject()["message"_L1].toString(); 0519 } 0520 0521 m_supportsLocalVisibility = obj.contains("pleroma"_L1); 0522 0523 m_instance_name = obj["title"_L1].toString(); 0524 0525 Q_EMIT fetchedInstanceMetadata(); 0526 }, 0527 [=](QNetworkReply *) { 0528 // Fall back to v1 instance information 0529 // TODO: a lot of this can be merged with v2 handling 0530 get(apiUrl(QStringLiteral("/api/v1/instance")), false, this, [=](QNetworkReply *reply) { 0531 if (200 != reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)) 0532 return; 0533 0534 const auto data = reply->readAll(); 0535 const auto doc = QJsonDocument::fromJson(data); 0536 0537 if (!doc.isObject()) 0538 return; 0539 0540 const auto obj = doc.object(); 0541 0542 if (obj.contains("configuration"_L1)) { 0543 const auto configObj = obj["configuration"_L1].toObject(); 0544 0545 if (configObj.contains("statuses"_L1)) { 0546 const auto statusConfigObj = configObj["statuses"_L1].toObject(); 0547 m_maxPostLength = statusConfigObj["max_characters"_L1].toInt(); 0548 m_charactersReservedPerUrl = statusConfigObj["characters_reserved_per_url"_L1].toInt(); 0549 } 0550 } 0551 0552 // One can only hope that there will always be a version attached 0553 if (obj.contains("version"_L1)) { 0554 m_allowedContentTypes = parseVersion(obj["version"_L1].toString()); 0555 } 0556 0557 // Pleroma/Akkoma may report maximum post characters here, instead 0558 if (obj.contains("max_toot_chars"_L1)) { 0559 m_maxPostLength = obj["max_toot_chars"_L1].toInt(); 0560 } 0561 0562 // Pleroma/Akkoma can report higher poll limits 0563 if (obj.contains("poll_limits"_L1)) { 0564 m_maxPollOptions = obj["poll_limits"_L1].toObject()["max_options"_L1].toInt(); 0565 } 0566 0567 // Other instance of poll options 0568 if (obj.contains("polls"_L1)) { 0569 m_maxPollOptions = obj["polls"_L1].toObject()["max_options"_L1].toInt(); 0570 } 0571 0572 m_registrationsOpen = obj["registrations"_L1].toBool(); 0573 0574 m_supportsLocalVisibility = obj.contains("pleroma"_L1); 0575 0576 m_instance_name = obj["title"_L1].toString(); 0577 0578 Q_EMIT fetchedInstanceMetadata(); 0579 }); 0580 }); 0581 0582 get(apiUrl(QStringLiteral("/nodeinfo/2.1.json")), false, this, [=](QNetworkReply *reply) { 0583 const auto data = reply->readAll(); 0584 const auto doc = QJsonDocument::fromJson(data); 0585 0586 m_allowedContentTypes = parsePleromaInfo(doc); 0587 Q_EMIT fetchedInstanceMetadata(); 0588 }); 0589 0590 fetchCustomEmojis(); 0591 } 0592 0593 void AbstractAccount::invalidate() 0594 { 0595 Q_EMIT invalidated(); 0596 } 0597 0598 void AbstractAccount::invalidatePost(Post *p) 0599 { 0600 Q_EMIT invalidatedPost(p); 0601 } 0602 0603 QUrl AbstractAccount::streamingUrl(const QString &stream) 0604 { 0605 QUrl url = apiUrl(QStringLiteral("/api/v1/streaming")); 0606 url.setQuery(QUrlQuery{ 0607 {QStringLiteral("access_token"), m_token}, 0608 {QStringLiteral("stream"), stream}, 0609 }); 0610 url.setScheme(QStringLiteral("wss")); 0611 0612 return url; 0613 } 0614 0615 void AbstractAccount::handleNotification(const QJsonDocument &doc) 0616 { 0617 const auto obj = doc.object(); 0618 std::shared_ptr<Notification> n = std::make_shared<Notification>(this, obj); 0619 0620 if (n->type() == Notification::FollowRequest) { 0621 m_hasFollowRequests = true; 0622 Q_EMIT hasFollowRequestsChanged(); 0623 } 0624 0625 Q_EMIT notification(n); 0626 } 0627 0628 void AbstractAccount::executeAction(Identity *identity, AccountAction accountAction, const QJsonObject &extraArguments) 0629 { 0630 const QHash<AccountAction, QString> accountActionMap = { 0631 {AccountAction::Follow, QStringLiteral("/follow")}, 0632 {AccountAction::Unfollow, QStringLiteral("/unfollow")}, 0633 {AccountAction::Block, QStringLiteral("/block")}, 0634 {AccountAction::Unblock, QStringLiteral("/unblock")}, 0635 {AccountAction::Mute, QStringLiteral("/mute")}, 0636 {AccountAction::Unmute, QStringLiteral("/unmute")}, 0637 {AccountAction::Feature, QStringLiteral("/pin")}, 0638 {AccountAction::Unfeature, QStringLiteral("/unpin")}, 0639 {AccountAction::Note, QStringLiteral("/note")}, 0640 }; 0641 0642 const auto apiCall = accountActionMap[accountAction]; 0643 0644 const auto accountId = identity->id(); 0645 const QString accountApiUrl = QStringLiteral("/api/v1/accounts/") + accountId + apiCall; 0646 const QJsonDocument doc(extraArguments); 0647 0648 post(apiUrl(accountApiUrl), doc, true, this, [=](QNetworkReply *reply) { 0649 auto doc = QJsonDocument::fromJson(reply->readAll()); 0650 auto jsonObj = doc.object(); 0651 0652 // Check if the request failed due to one account blocking the other 0653 if (!jsonObj.value("error"_L1).isUndefined()) { 0654 const QHash<AccountAction, QString> accountActionMap = { 0655 {AccountAction::Follow, i18n("Could not follow account")}, 0656 {AccountAction::Unfollow, i18n("Could not unfollow account")}, 0657 {AccountAction::Block, i18n("Could not block account")}, 0658 {AccountAction::Unblock, i18n("Could not unblock account")}, 0659 {AccountAction::Mute, i18n("Could not mute account")}, 0660 {AccountAction::Unmute, i18n("Could not unmute account")}, 0661 {AccountAction::Feature, i18n("Could not feature account")}, 0662 {AccountAction::Unfeature, i18n("Could not unfeature account")}, 0663 {AccountAction::Note, i18n("Could not edit note about an account")}, 0664 }; 0665 const auto errorMessage = accountActionMap[accountAction]; 0666 Q_EMIT errorOccured(errorMessage); 0667 return; 0668 } 0669 // If returned json obj is not an error, it's a relationship status. 0670 // Returned relationship should have a value of true 0671 // under either the "following" or "requested" keys. 0672 auto relationship = identity->relationship(); 0673 relationship->updateFromJson(jsonObj); 0674 0675 Q_EMIT identity->relationshipChanged(); 0676 }); 0677 } 0678 0679 void AbstractAccount::followAccount(Identity *identity, bool reblogs, bool notify) 0680 { 0681 executeAction(identity, 0682 AccountAction::Follow, 0683 { 0684 {QStringLiteral("reblogs"), reblogs}, 0685 {QStringLiteral("notify"), notify}, 0686 }); 0687 } 0688 0689 void AbstractAccount::unfollowAccount(Identity *identity) 0690 { 0691 executeAction(identity, AccountAction::Unfollow); 0692 } 0693 0694 void AbstractAccount::blockAccount(Identity *identity) 0695 { 0696 executeAction(identity, AccountAction::Block); 0697 } 0698 0699 void AbstractAccount::unblockAccount(Identity *identity) 0700 { 0701 executeAction(identity, AccountAction::Unblock); 0702 } 0703 0704 void AbstractAccount::muteAccount(Identity *identity, bool notifications, int duration) 0705 { 0706 executeAction(identity, AccountAction::Mute, {{QStringLiteral("notifcations"), notifications}, {QStringLiteral("duration"), duration}}); 0707 } 0708 0709 void AbstractAccount::unmuteAccount(Identity *identity) 0710 { 0711 executeAction(identity, AccountAction::Unmute); 0712 } 0713 0714 void AbstractAccount::featureAccount(Identity *identity) 0715 { 0716 executeAction(identity, AccountAction::Feature); 0717 } 0718 0719 void AbstractAccount::unfeatureAccount(Identity *identity) 0720 { 0721 executeAction(identity, AccountAction::Unfeature); 0722 } 0723 0724 void AbstractAccount::addNote(Identity *identity, const QString ¬e) 0725 { 0726 if (note.isEmpty()) { 0727 executeAction(identity, AccountAction::Note); 0728 } else { 0729 executeAction(identity, AccountAction::Note, {{QStringLiteral("comment"), note}}); 0730 } 0731 } 0732 0733 void AbstractAccount::mutateRemotePost(const QString &url, const QString &verb) 0734 { 0735 NetworkController::instance().requestRemoteObject(this, url, [=](QNetworkReply *reply) { 0736 const auto searchResult = QJsonDocument::fromJson(reply->readAll()).object(); 0737 const auto statuses = searchResult[QStringLiteral("statuses")].toArray(); 0738 const auto accounts = searchResult[QStringLiteral("accounts")].toArray(); 0739 0740 // TODO: emit error when the mutation has failed, no post is available on this account's server 0741 if (!statuses.isEmpty()) { 0742 const auto status = statuses[0].toObject(); 0743 0744 if (verb == QStringLiteral("reply")) { 0745 // TODO: we can't delete this immediately, will need some smarter cleanup in the PostEditorBackend 0746 Post *post = new Post(this, this); 0747 post->fromJson(status); 0748 Q_EMIT Navigation::instance().replyTo(post->postId(), post->mentions(), post->visibility(), post->getAuthorIdentity(), post); 0749 } else { 0750 const QString localID = status["id"_L1].toString(); 0751 mutatePost(localID, verb); 0752 } 0753 } 0754 }); 0755 } 0756 0757 void AbstractAccount::fetchOEmbed(const QString &id, Identity *identity) 0758 { 0759 QUrlQuery query; 0760 query.addQueryItem(QStringLiteral("url"), QStringLiteral("%1/@%2/%3").arg(m_instance_uri, identity->username(), id)); 0761 0762 QUrl oembedUrl = apiUrl(QStringLiteral("/api/oembed")); 0763 oembedUrl.setQuery(query); 0764 0765 get(oembedUrl, false, this, [this](QNetworkReply *reply) { 0766 const auto doc = QJsonDocument::fromJson(reply->readAll()); 0767 if (doc.object().contains("html"_L1)) { 0768 Q_EMIT fetchedOEmbed(doc.object()["html"_L1].toString()); 0769 } 0770 }); 0771 } 0772 0773 bool AbstractAccount::isRegistered() const 0774 { 0775 return !m_client_id.isEmpty() && !m_client_secret.isEmpty(); 0776 } 0777 0778 bool AbstractAccount::registrationsOpen() const 0779 { 0780 return m_registrationsOpen; 0781 } 0782 0783 QString AbstractAccount::registrationMessage() const 0784 { 0785 return m_registrationMessage; 0786 } 0787 0788 QString AbstractAccount::settingsGroupName() const 0789 { 0790 return AccountManager::settingsGroupName(m_name, m_instance_uri); 0791 } 0792 0793 QString AbstractAccount::clientSecretKey() const 0794 { 0795 return AccountManager::clientSecretKey(settingsGroupName()); 0796 } 0797 0798 QString AbstractAccount::accessTokenKey() const 0799 { 0800 return AccountManager::accessTokenKey(settingsGroupName()); 0801 } 0802 0803 void AbstractAccount::fetchCustomEmojis() 0804 { 0805 m_customEmojis.clear(); 0806 0807 get(apiUrl(QStringLiteral("/api/v1/custom_emojis")), false, this, [=](QNetworkReply *reply) { 0808 if (200 != reply->attribute(QNetworkRequest::HttpStatusCodeAttribute)) 0809 return; 0810 0811 const auto data = reply->readAll(); 0812 const auto doc = QJsonDocument::fromJson(data); 0813 0814 if (!doc.isArray()) 0815 return; 0816 0817 const auto array = doc.array(); 0818 0819 for (auto emojiObj : array) { 0820 if (!emojiObj.isObject()) { 0821 continue; 0822 } 0823 0824 CustomEmoji customEmoji{}; 0825 customEmoji.shortcode = emojiObj[QStringLiteral("shortcode")].toString(); 0826 customEmoji.url = emojiObj[QStringLiteral("url")].toString(); 0827 0828 m_customEmojis.push_back(customEmoji); 0829 } 0830 0831 Q_EMIT fetchedCustomEmojis(); 0832 }); 0833 } 0834 0835 QList<CustomEmoji> AbstractAccount::customEmojis() const 0836 { 0837 return m_customEmojis; 0838 } 0839 0840 #include "moc_abstractaccount.cpp"