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 &note)
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"