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