File indexing completed on 2024-05-12 16:28:10

0001 // SPDX-FileCopyrightText: 2021 kaniini <https://git.pleroma.social/kaniini>
0002 // SPDX-License-Identifier: GPL-3.0-only
0003 
0004 #include <KLocalizedString>
0005 #include <QFile>
0006 #include <QFileInfo>
0007 #include <QMap>
0008 #include <QMimeDatabase>
0009 #include <QtMath>
0010 #include <qurl.h>
0011 
0012 #include "account/abstractaccount.h"
0013 #include "poll.h"
0014 #include "post.h"
0015 #include "utils/utils.h"
0016 
0017 static QMap<QString, Attachment::AttachmentType> stringToAttachmentType = {
0018     {"image", Attachment::AttachmentType::Image},
0019     {"gifv", Attachment::AttachmentType::GifV},
0020     {"video", Attachment::AttachmentType::Video},
0021     {"unknown", Attachment::AttachmentType::Unknown},
0022 };
0023 
0024 Attachment::Attachment(QObject *parent)
0025     : QObject(parent)
0026 {
0027 }
0028 
0029 Attachment::Attachment(const QJsonObject &obj, QObject *parent)
0030     : QObject(parent)
0031 {
0032     fromJson(obj);
0033 }
0034 
0035 void Attachment::fromJson(const QJsonObject &obj)
0036 {
0037     if (!obj.contains("type")) {
0038         m_type = Unknown;
0039         return;
0040     }
0041 
0042     m_id = obj["id"].toString();
0043     m_url = obj["url"].toString();
0044     m_preview_url = obj["preview_url"].toString();
0045     m_remote_url = obj["remote_url"].toString();
0046 
0047     setDescription(obj["description"].toString());
0048     m_blurhash = obj["blurhash"].toString();
0049     m_sourceHeight = obj["meta"].toObject()["original"].toObject()["height"].toInt();
0050     m_sourceWidth = obj["meta"].toObject()["original"].toObject()["width"].toInt();
0051 
0052     // determine type if we can
0053     const auto type = obj["type"].toString();
0054     if (stringToAttachmentType.contains(type)) {
0055         m_type = stringToAttachmentType[type];
0056     }
0057 
0058     const auto mimeType = QMimeDatabase().mimeTypeForFile(m_remote_url);
0059     if (m_type == AttachmentType::Unknown && mimeType.name().contains("image")) {
0060         m_type = AttachmentType::Image;
0061     }
0062 }
0063 
0064 QString Post::type() const
0065 {
0066     return "post";
0067 }
0068 
0069 QString Attachment::description() const
0070 {
0071     return m_description;
0072 }
0073 
0074 void Attachment::setDescription(const QString &description)
0075 {
0076     m_description = description;
0077 }
0078 
0079 QString Attachment::id() const
0080 {
0081     return m_id;
0082 }
0083 
0084 int Attachment::isVideo() const
0085 {
0086     if (m_type == AttachmentType::GifV || m_type == AttachmentType::Video) {
0087         return 1;
0088     }
0089 
0090     return 0;
0091 }
0092 
0093 QString Attachment::tempSource() const
0094 {
0095     return QString("image://blurhash/%1").arg(m_blurhash);
0096 }
0097 
0098 Post::Post(AbstractAccount *account, QObject *parent)
0099     : QObject(parent)
0100     , m_parent(account)
0101     , m_attachmentList(this, &m_attachments)
0102 {
0103     QString visibilityString = account->identity()->visibility();
0104     m_visibility = stringToVisibility(visibilityString);
0105 }
0106 
0107 QString computeContent(const QJsonObject &obj, std::shared_ptr<Identity> authorIdentity)
0108 {
0109     QString content = obj["content"].toString();
0110     const auto emojis = obj["emojis"].toArray();
0111 
0112     for (const auto &emoji : emojis) {
0113         const auto emojiObj = emoji.toObject();
0114         content = content.replace(QLatin1Char(':') + emojiObj["shortcode"].toString() + QLatin1Char(':'),
0115                                   "<img height=\"16\" align=\"middle\" width=\"16\" src=\"" + emojiObj["static_url"].toString() + "\">");
0116     }
0117 
0118     const auto tags = obj["tags"].toArray();
0119     const QString baseUrl = authorIdentity->url().toDisplayString(QUrl::RemovePath);
0120 
0121     for (const auto &tag : tags) {
0122         const auto tagObj = tag.toObject();
0123         content = content.replace(baseUrl + QStringLiteral("/tags/") + tagObj["name"].toString(),
0124                                   QStringLiteral("hashtag:/") + tagObj["name"].toString(),
0125                                   Qt::CaseInsensitive);
0126     }
0127 
0128     const auto mentions = obj["mentions"].toArray();
0129 
0130     for (const auto &mention : mentions) {
0131         const auto mentionObj = mention.toObject();
0132         content = content.replace(mentionObj["url"].toString(), QStringLiteral("account:/") + mentionObj["id"].toString(), Qt::CaseInsensitive);
0133     }
0134 
0135     return content;
0136 }
0137 
0138 Post::Post(AbstractAccount *account, QJsonObject obj, QObject *parent)
0139     : QObject(parent)
0140     , m_parent(account)
0141     , m_attachmentList(this, &m_attachments)
0142     , m_visibility(Post::Visibility::Public)
0143 {
0144     fromJson(obj);
0145 }
0146 
0147 Post *Notification::createPost(AbstractAccount *account, const QJsonObject &obj, QObject *parent)
0148 {
0149     if (!obj.empty()) {
0150         return new Post(account, obj, parent);
0151     }
0152 
0153     return nullptr;
0154 }
0155 
0156 void Post::fromJson(QJsonObject obj)
0157 {
0158     const auto accountDoc = obj["account"].toObject();
0159     const auto accountId = accountDoc["id"].toString();
0160 
0161     m_originalPostId = obj["id"].toString();
0162     const auto reblogObj = obj["reblog"].toObject();
0163 
0164     if (!obj.contains("reblog") || reblogObj.isEmpty()) {
0165         m_boosted = false;
0166         m_authorIdentity = m_parent->identityLookup(accountId, accountDoc);
0167     } else {
0168         m_boosted = true;
0169 
0170         const auto reblogAccountDoc = reblogObj["account"].toObject();
0171         const auto reblogAccountId = reblogAccountDoc["id"].toString();
0172 
0173         m_authorIdentity = m_parent->identityLookup(reblogAccountId, reblogAccountDoc);
0174         m_boostIdentity = m_parent->identityLookup(accountId, accountDoc);
0175 
0176         obj = reblogObj;
0177     }
0178 
0179     m_postId = obj["id"].toString();
0180 
0181     m_spoilerText = obj["spoiler_text"].toString();
0182     m_content = computeContent(obj, m_authorIdentity);
0183 
0184     m_replyTargetId = obj["in_reply_to_id"].toString();
0185 
0186     if (obj.contains("in_reply_to_account_id")) {
0187         if (m_parent->identityCached(obj["in_reply_to_account_id"].toString())) {
0188             m_replyIdentity = m_parent->identityLookup(obj["in_reply_to_account_id"].toString(), {});
0189         } else if (!obj["in_reply_to_account_id"].toString().isEmpty()) {
0190             const auto accountId = obj["in_reply_to_account_id"].toString();
0191             QUrl uriAccount(m_parent->instanceUri());
0192             uriAccount.setPath(QStringLiteral("/api/v1/accounts/%1").arg(accountId));
0193 
0194             m_parent->get(uriAccount, true, this, [this, accountId](QNetworkReply *reply) {
0195                 const auto data = reply->readAll();
0196                 const auto doc = QJsonDocument::fromJson(data);
0197 
0198                 m_replyIdentity = m_parent->identityLookup(accountId, doc.object());
0199                 Q_EMIT replyIdentityChanged();
0200             });
0201         }
0202     }
0203 
0204     m_url = QUrl(obj["url"].toString());
0205 
0206     m_favouritesCount = obj["favourites_count"].toInt();
0207     m_reblogsCount = obj["reblogs_count"].toInt();
0208     m_repliesCount = obj["replies_count"].toInt();
0209 
0210     m_favourited = obj["favourited"].toBool();
0211     m_reblogged = obj["reblogged"].toBool();
0212     m_bookmarked = obj["bookmarked"].toBool();
0213     m_pinned = obj["pinned"].toBool();
0214     m_muted = obj["muted"].toBool();
0215 
0216     m_filters.clear();
0217 
0218     const auto filters = obj["filtered"].toArray();
0219     for (const auto &filter : filters) {
0220         const auto filterContext = filter.toObject();
0221         const auto filterObj = filterContext["filter"].toObject();
0222         m_filters << filterObj["title"].toString();
0223 
0224         const auto filterAction = filterObj["filter_action"];
0225         if (filterAction == "warn") {
0226             m_filtered = true;
0227         } else if (filterAction == "hide") {
0228             m_hidden = true;
0229         }
0230     }
0231 
0232     m_sensitive = obj["sensitive"].toBool();
0233     m_visibility = stringToVisibility(obj["visibility"].toString());
0234     m_language = obj["language"].toString();
0235 
0236     m_publishedAt = QDateTime::fromString(obj["created_at"].toString(), Qt::ISODate).toLocalTime();
0237 
0238     m_attachments.clear();
0239     addAttachments(obj["media_attachments"].toArray());
0240     const QJsonArray mentions = obj["mentions"].toArray();
0241     if (obj.contains("card") && !obj["card"].toObject().empty()) {
0242         setCard(std::make_optional<Card>(obj["card"].toObject()));
0243     }
0244 
0245     if (obj.contains("application") && !obj["application"].toObject().empty()) {
0246         setApplication(std::make_optional<Application>(obj["application"].toObject()));
0247     }
0248 
0249     m_mentions.clear();
0250     for (const auto &m : qAsConst(mentions)) {
0251         const QJsonObject o = m.toObject();
0252         m_mentions.push_back("@" + o["acct"].toString());
0253     }
0254 
0255     if (obj.contains(QStringLiteral("poll")) && !obj[QStringLiteral("poll")].isNull()) {
0256         m_poll = std::make_unique<Poll>(obj[QStringLiteral("poll")].toObject());
0257     }
0258 
0259     m_attachments_visible = !m_sensitive;
0260 }
0261 
0262 void Post::addAttachments(const QJsonArray &attachments)
0263 {
0264     for (const auto &attachment : attachments) {
0265         m_attachments.append(new Attachment{attachment.toObject(), this});
0266     }
0267 }
0268 
0269 void Post::addAttachment(const QJsonObject &attachment)
0270 {
0271     auto att = new Attachment{attachment, this};
0272     if (att->m_url.isEmpty()) {
0273         return;
0274     }
0275     m_attachments.append(att);
0276 
0277     Q_EMIT attachmentUploaded();
0278 }
0279 
0280 void Post::setInReplyTo(const QString &inReplyTo)
0281 {
0282     if (inReplyTo == m_replyTargetId) {
0283         return;
0284     }
0285     m_replyTargetId = inReplyTo;
0286     Q_EMIT inReplyToChanged();
0287 }
0288 
0289 int Post::repliesCount() const
0290 {
0291     return m_repliesCount;
0292 }
0293 
0294 QString Post::inReplyTo() const
0295 {
0296     return m_replyTargetId;
0297 }
0298 
0299 void Post::setDirtyAttachment()
0300 {
0301     m_parent->invalidatePost(this);
0302 }
0303 
0304 QStringList Post::mentions() const
0305 {
0306     return m_mentions;
0307 }
0308 
0309 QStringList Post::filters() const
0310 {
0311     return m_filters;
0312 }
0313 
0314 QUrl Post::url() const
0315 {
0316     return m_url;
0317 }
0318 
0319 void Post::setMentions(const QStringList &mentions)
0320 {
0321     if (mentions == m_mentions) {
0322         return;
0323     }
0324     m_mentions = mentions;
0325     Q_EMIT mentionsChanged();
0326 }
0327 
0328 QDateTime Post::publishedAt() const
0329 {
0330     return m_publishedAt;
0331 }
0332 
0333 QString Post::relativeTime() const
0334 {
0335     const auto current = QDateTime::currentDateTime();
0336     const auto publishingDate = publishedAt();
0337     const auto secsTo = publishingDate.secsTo(current);
0338     const auto daysTo = publishingDate.daysTo(current);
0339     if (secsTo < 0) {
0340         return i18n("in the future");
0341     } else if (secsTo < 60) {
0342         return i18n("%1s", qCeil(secsTo));
0343     } else if (secsTo < 60 * 60) {
0344         return i18n("%1m", qCeil(secsTo / 60));
0345     } else if (secsTo < 60 * 60 * 24) {
0346         return i18n("%1h", qCeil(secsTo / (60 * 60)));
0347     } else if (daysTo < 7) {
0348         return i18n("%1d", qCeil(daysTo));
0349     } else if (daysTo < 365) {
0350         const auto weeksTo = qCeil(daysTo / 7);
0351         if (weeksTo < 5) {
0352             return i18np("1 week ago", "%1 weeks ago", weeksTo);
0353         } else {
0354             const auto monthsTo = qCeil(daysTo / 30);
0355             return i18np("1 month ago", "%1 months ago", monthsTo);
0356         }
0357     } else {
0358         const auto yearsTo = qCeil(daysTo / 365);
0359         return i18np("1 year ago", "%1 years ago", yearsTo);
0360     }
0361 }
0362 
0363 QString Post::absoluteTime() const
0364 {
0365     return QLocale::system().toString(publishedAt(), QLocale::LongFormat);
0366 }
0367 
0368 int Post::favouritesCount() const
0369 {
0370     return m_favouritesCount;
0371 }
0372 
0373 int Post::reblogsCount() const
0374 {
0375     return m_reblogsCount;
0376 }
0377 
0378 static QMap<QString, Notification::Type> str_to_not_type = {
0379     {"favourite", Notification::Type::Favorite},
0380     {"follow", Notification::Type::Follow},
0381     {"mention", Notification::Type::Mention},
0382     {"reblog", Notification::Type::Repeat},
0383     {"update", Notification::Type::Update},
0384     {"poll", Notification::Type::Poll},
0385 };
0386 
0387 Notification::Notification(AbstractAccount *account, const QJsonObject &obj, QObject *parent)
0388     : m_account(account)
0389 {
0390     const auto accountObj = obj["account"].toObject();
0391     const auto status = obj["status"].toObject();
0392     const auto accountId = accountObj["id"].toString();
0393     const auto type = obj["type"].toString();
0394 
0395     m_post = createPost(m_account, status, parent);
0396     m_identity = m_account->identityLookup(accountId, accountObj);
0397     m_type = str_to_not_type[type];
0398     m_id = obj["id"].toString().toInt();
0399 }
0400 
0401 int Notification::id() const
0402 {
0403     return m_id;
0404 }
0405 
0406 AbstractAccount *Notification::account() const
0407 {
0408     return m_account;
0409 }
0410 
0411 Notification::Type Notification::type() const
0412 {
0413     return m_type;
0414 }
0415 
0416 Post *Notification::post() const
0417 {
0418     return m_post;
0419 }
0420 
0421 std::shared_ptr<Identity> Notification::identity() const
0422 {
0423     return m_identity;
0424 }
0425 
0426 QString Post::spoilerText() const
0427 {
0428     return m_spoilerText;
0429 }
0430 
0431 void Post::setSpoilerText(const QString &spoilerText)
0432 {
0433     if (spoilerText == m_spoilerText) {
0434         return;
0435     }
0436     m_spoilerText = spoilerText;
0437     Q_EMIT spoilerTextChanged();
0438 }
0439 
0440 QString Post::content() const
0441 {
0442     return m_content;
0443 }
0444 
0445 void Post::setContent(const QString &content)
0446 {
0447     if (content == m_content) {
0448         return;
0449     }
0450     m_content = content;
0451     Q_EMIT contentChanged();
0452 }
0453 
0454 QString Post::contentType() const
0455 {
0456     return m_content_type;
0457 }
0458 
0459 void Post::setContentType(const QString &contentType)
0460 {
0461     if (m_content_type == contentType) {
0462         return;
0463     }
0464     m_content_type = contentType;
0465     Q_EMIT contentTypeChanged();
0466 }
0467 
0468 bool Post::sensitive() const
0469 {
0470     return m_sensitive;
0471 }
0472 
0473 void Post::setSensitive(bool sensitive)
0474 {
0475     if (m_sensitive == sensitive) {
0476         return;
0477     }
0478     m_sensitive = sensitive;
0479     Q_EMIT sensitiveChanged();
0480 }
0481 
0482 Post::Visibility Post::visibility() const
0483 {
0484     return m_visibility;
0485 }
0486 
0487 void Post::setVisibility(Visibility visibility)
0488 {
0489     if (visibility == m_visibility) {
0490         return;
0491     }
0492     m_visibility = visibility;
0493     Q_EMIT visibilityChanged();
0494 }
0495 
0496 QString Post::language() const
0497 {
0498     return m_language;
0499 }
0500 
0501 void Post::setLanguage(const QString &language)
0502 {
0503     if (language == m_language) {
0504         return;
0505     }
0506     m_language = language;
0507     Q_EMIT languageChanged();
0508 }
0509 
0510 std::optional<Card> Post::card() const
0511 {
0512     return m_card;
0513 }
0514 
0515 Card *Post::getCard() const
0516 {
0517     if (m_card.has_value()) {
0518         return const_cast<Card *>(&m_card.value());
0519     } else {
0520         return nullptr;
0521     }
0522 }
0523 
0524 void Post::setCard(std::optional<Card> card)
0525 {
0526     m_card = card;
0527 }
0528 
0529 std::optional<Application> Post::application() const
0530 {
0531     return m_application;
0532 }
0533 
0534 void Post::setApplication(std::optional<Application> application)
0535 {
0536     m_application = application;
0537 }
0538 
0539 bool Post::favourited() const
0540 {
0541     return m_favourited;
0542 }
0543 
0544 void Post::setFavourited(bool favourited)
0545 {
0546     m_favourited = favourited;
0547 }
0548 
0549 bool Post::reblogged() const
0550 {
0551     return m_reblogged;
0552 }
0553 
0554 void Post::setReblogged(bool reblogged)
0555 {
0556     m_reblogged = reblogged;
0557 }
0558 
0559 bool Post::muted() const
0560 {
0561     return m_muted;
0562 }
0563 
0564 void Post::setMuted(bool muted)
0565 {
0566     m_muted = muted;
0567 }
0568 
0569 bool Post::bookmarked() const
0570 {
0571     return m_bookmarked;
0572 }
0573 
0574 void Post::setBookmarked(bool bookmarked)
0575 {
0576     m_bookmarked = bookmarked;
0577 }
0578 
0579 bool Post::pinned() const
0580 {
0581     return m_pinned;
0582 }
0583 
0584 void Post::setPinned(bool pinned)
0585 {
0586     m_pinned = pinned;
0587 }
0588 
0589 bool Post::filtered() const
0590 {
0591     return m_filtered;
0592 }
0593 
0594 QList<Attachment *> Post::attachments() const
0595 {
0596     return m_attachments;
0597 }
0598 
0599 QQmlListProperty<Attachment> Post::attachmentList() const
0600 {
0601     return m_attachmentList;
0602 }
0603 
0604 void Post::setAttachmentsVisible(bool attachmentsVisible)
0605 {
0606     m_attachments_visible = attachmentsVisible;
0607 }
0608 
0609 bool Post::attachmentsVisible() const
0610 {
0611     return m_attachments_visible;
0612 }
0613 
0614 bool Post::boosted() const
0615 {
0616     return m_boosted;
0617 }
0618 
0619 Card::Card(QJsonObject card)
0620     : m_card(card)
0621 {
0622 }
0623 
0624 QString Card::authorName() const
0625 {
0626     return m_card[QLatin1String("author_name")].toString();
0627 }
0628 
0629 QString Card::authorUrl() const
0630 {
0631     return m_card[QLatin1String("author_url")].toString();
0632 }
0633 
0634 QString Card::blurhash() const
0635 {
0636     return m_card[QLatin1String("blurhash")].toString();
0637 }
0638 
0639 QString Card::description() const
0640 {
0641     return m_card[QLatin1String("description")].toString();
0642 }
0643 
0644 QString Card::embedUrl() const
0645 {
0646     return m_card[QLatin1String("embed_url")].toString();
0647 }
0648 
0649 int Card::width() const
0650 {
0651     return m_card[QLatin1String("weight")].toInt();
0652 }
0653 
0654 int Card::height() const
0655 {
0656     return m_card[QLatin1String("height")].toInt();
0657 }
0658 
0659 QString Card::html() const
0660 {
0661     return m_card[QLatin1String("html")].toString();
0662 }
0663 
0664 QString Card::image() const
0665 {
0666     return m_card[QLatin1String("image")].toString();
0667 }
0668 
0669 QString Card::providerName() const
0670 {
0671     const auto providerName = m_card[QLatin1String("provider_name")].toString();
0672     if (!providerName.isEmpty()) {
0673         return providerName;
0674     }
0675     return url().host();
0676 }
0677 
0678 QString Card::providerUrl() const
0679 {
0680     return m_card[QLatin1String("provider_url")].toString();
0681 }
0682 
0683 QString Card::title() const
0684 {
0685     return m_card[QLatin1String("title")].toString().trimmed();
0686 }
0687 
0688 QUrl Card::url() const
0689 {
0690     return QUrl::fromUserInput(m_card[QLatin1String("url")].toString());
0691 }
0692 
0693 Application::Application(QJsonObject application)
0694     : m_application(application)
0695 {
0696 }
0697 
0698 QString Application::name() const
0699 {
0700     return m_application[QLatin1String("name")].toString();
0701 }
0702 
0703 QUrl Application::website() const
0704 {
0705     return QUrl::fromUserInput(m_application[QLatin1String("website")].toString());
0706 }
0707 
0708 Identity *Post::getAuthorIdentity() const
0709 {
0710     return authorIdentity().get();
0711 }
0712 
0713 std::shared_ptr<Identity> Post::authorIdentity() const
0714 {
0715     return m_authorIdentity;
0716 }
0717 
0718 std::shared_ptr<Identity> Post::boostIdentity() const
0719 {
0720     return m_boostIdentity;
0721 }
0722 
0723 std::shared_ptr<Identity> Post::replyIdentity() const
0724 {
0725     return m_replyIdentity;
0726 }
0727 
0728 Poll *Post::poll() const
0729 {
0730     return m_poll.get();
0731 }
0732 
0733 void Post::setPollJson(const QJsonObject &object)
0734 {
0735     m_poll = std::make_unique<Poll>(object);
0736     Q_EMIT pollChanged();
0737 }
0738 
0739 QString Post::postId() const
0740 {
0741     return m_postId;
0742 }
0743 
0744 QString Post::originalPostId() const
0745 {
0746     return m_originalPostId;
0747 }
0748 
0749 bool Post::isEmpty() const
0750 {
0751     return m_postId.isEmpty();
0752 }