File indexing completed on 2024-05-12 05:04:16

0001 // SPDX-FileCopyrightText: 2021 kaniini <https://git.pleroma.social/kaniini>
0002 // SPDX-License-Identifier: GPL-3.0-only
0003 
0004 #include "post.h"
0005 
0006 #include "account/abstractaccount.h"
0007 #include "utils/utils.h"
0008 
0009 #include <KLocalizedString>
0010 
0011 using namespace Qt::Literals::StringLiterals;
0012 
0013 static QMap<QString, Attachment::AttachmentType> stringToAttachmentType = {
0014     {QStringLiteral("image"), Attachment::AttachmentType::Image},
0015     {QStringLiteral("gifv"), Attachment::AttachmentType::GifV},
0016     {QStringLiteral("video"), Attachment::AttachmentType::Video},
0017     {QStringLiteral("unknown"), Attachment::AttachmentType::Unknown},
0018 };
0019 
0020 Attachment::Attachment(QObject *parent)
0021     : QObject(parent)
0022 {
0023 }
0024 
0025 Attachment::Attachment(const QJsonObject &obj, QObject *parent)
0026     : QObject(parent)
0027 {
0028     fromJson(obj);
0029 }
0030 
0031 void Attachment::fromJson(const QJsonObject &obj)
0032 {
0033     if (!obj.contains("type"_L1)) {
0034         m_type = Unknown;
0035         return;
0036     }
0037 
0038     m_id = obj["id"_L1].toString();
0039     m_url = obj["url"_L1].toString();
0040     m_preview_url = obj["preview_url"_L1].toString();
0041     m_remote_url = obj["remote_url"_L1].toString();
0042 
0043     setDescription(obj["description"_L1].toString());
0044     m_blurhash = obj["blurhash"_L1].toString();
0045     m_sourceHeight = obj["meta"_L1].toObject()["original"_L1].toObject()["height"_L1].toInt();
0046     m_sourceWidth = obj["meta"_L1].toObject()["original"_L1].toObject()["width"_L1].toInt();
0047 
0048     // determine type if we can
0049     const auto type = obj["type"_L1].toString();
0050     if (stringToAttachmentType.contains(type)) {
0051         m_type = stringToAttachmentType[type];
0052     }
0053 
0054     if (!m_remote_url.isEmpty()) {
0055         const auto mimeType = QMimeDatabase().mimeTypeForFile(m_remote_url);
0056         if (m_type == AttachmentType::Unknown && mimeType.name().contains("image"_L1)) {
0057             m_type = AttachmentType::Image;
0058         }
0059     }
0060 
0061     if (obj.contains("meta"_L1) && obj["meta"_L1].toObject().contains("focus"_L1)) {
0062         m_focusX = obj["meta"_L1].toObject()["focus"_L1].toObject()["x"_L1].toDouble();
0063         m_focusY = obj["meta"_L1].toObject()["focus"_L1].toObject()["y"_L1].toDouble();
0064     }
0065 }
0066 
0067 QString Post::type() const
0068 {
0069     return QStringLiteral("post");
0070 }
0071 
0072 QString Attachment::description() const
0073 {
0074     return m_description;
0075 }
0076 
0077 void Attachment::setDescription(const QString &description)
0078 {
0079     m_description = description;
0080 }
0081 
0082 QString Attachment::id() const
0083 {
0084     return m_id;
0085 }
0086 
0087 int Attachment::isVideo() const
0088 {
0089     if (m_type == AttachmentType::GifV || m_type == AttachmentType::Video) {
0090         return 1;
0091     }
0092 
0093     return 0;
0094 }
0095 
0096 QString Attachment::tempSource() const
0097 {
0098     return QStringLiteral("image://blurhash/%1").arg(m_blurhash);
0099 }
0100 
0101 double Attachment::focusX() const
0102 {
0103     return m_focusX;
0104 }
0105 
0106 void Attachment::setFocusX(double value)
0107 {
0108     if (value != m_focusX) {
0109         m_focusX = value;
0110         Q_EMIT focusXChanged();
0111     }
0112 }
0113 
0114 double Attachment::focusY() const
0115 {
0116     return m_focusY;
0117 }
0118 
0119 void Attachment::setFocusY(double value)
0120 {
0121     if (value != m_focusY) {
0122         m_focusY = value;
0123         Q_EMIT focusYChanged();
0124     }
0125 }
0126 
0127 Post::Post(AbstractAccount *account, QObject *parent)
0128     : QObject(parent)
0129     , m_parent(account)
0130     , m_attachmentList(this, &m_attachments)
0131 {
0132     QString visibilityString = account->identity()->visibility();
0133     m_visibility = stringToVisibility(visibilityString);
0134 }
0135 
0136 QString computeContent(const QJsonObject &obj, std::shared_ptr<Identity> authorIdentity)
0137 {
0138     const auto emojis = CustomEmoji::parseCustomEmojis(obj["emojis"_L1].toArray());
0139     QString content = CustomEmoji::replaceCustomEmojis(emojis, obj["content"_L1].toString());
0140 
0141     const auto tags = obj["tags"_L1].toArray();
0142     const QString baseUrl = authorIdentity->url().toDisplayString(QUrl::RemovePath);
0143 
0144     for (const auto &tag : tags) {
0145         const auto tagObj = tag.toObject();
0146 
0147         const QList<QString> tagFormats = {
0148             QStringLiteral("tags"), // Mastodon
0149             QStringLiteral("tag") // Akkoma/Pleroma
0150         };
0151 
0152         for (const QString &tagFormat : tagFormats) {
0153             content = content.replace(baseUrl + QStringLiteral("/%1/").arg(tagFormat) + tagObj["name"_L1].toString(),
0154                                       QStringLiteral("hashtag:/") + tagObj["name"_L1].toString(),
0155                                       Qt::CaseInsensitive);
0156         }
0157     }
0158 
0159     const auto mentions = obj["mentions"_L1].toArray();
0160 
0161     for (const auto &mention : mentions) {
0162         const auto mentionObj = mention.toObject();
0163         content = content.replace(mentionObj["url"_L1].toString(), QStringLiteral("account:/") + mentionObj["id"_L1].toString(), Qt::CaseInsensitive);
0164     }
0165 
0166     return content;
0167 }
0168 
0169 Post::Post(AbstractAccount *account, QJsonObject obj, QObject *parent)
0170     : QObject(parent)
0171     , m_parent(account)
0172     , m_attachmentList(this, &m_attachments)
0173     , m_visibility(Post::Visibility::Public)
0174 {
0175     fromJson(obj);
0176 }
0177 
0178 Post *Notification::createPost(AbstractAccount *account, const QJsonObject &obj, QObject *parent)
0179 {
0180     if (!obj.empty()) {
0181         return new Post(account, obj, parent);
0182     }
0183 
0184     return nullptr;
0185 }
0186 
0187 void Post::fromJson(QJsonObject obj)
0188 {
0189     const auto accountDoc = obj["account"_L1].toObject();
0190     const auto accountId = accountDoc["id"_L1].toString();
0191 
0192     m_originalPostId = obj["id"_L1].toString();
0193     const auto reblogObj = obj["reblog"_L1].toObject();
0194 
0195     if (!obj.contains("reblog"_L1) || reblogObj.isEmpty()) {
0196         m_boosted = false;
0197         m_authorIdentity = m_parent->identityLookup(accountId, accountDoc);
0198     } else {
0199         m_boosted = true;
0200 
0201         const auto reblogAccountDoc = reblogObj["account"_L1].toObject();
0202         const auto reblogAccountId = reblogAccountDoc["id"_L1].toString();
0203 
0204         m_authorIdentity = m_parent->identityLookup(reblogAccountId, reblogAccountDoc);
0205         m_boostIdentity = m_parent->identityLookup(accountId, accountDoc);
0206 
0207         obj = reblogObj;
0208     }
0209 
0210     m_postId = obj["id"_L1].toString();
0211 
0212     m_spoilerText = obj["spoiler_text"_L1].toString();
0213 
0214     // First process HTML for links, and custom emojis
0215     const QString computedContent = computeContent(obj, m_authorIdentity);
0216 
0217     // And then parse the content for standalone tags
0218     auto [processedContent, standaloneTags] = parseContent(computedContent);
0219     m_content = processedContent;
0220     m_standaloneTags = standaloneTags;
0221 
0222     m_replyTargetId = obj["in_reply_to_id"_L1].toString();
0223 
0224     if (obj.contains("in_reply_to_account_id"_L1) && obj["in_reply_to_account_id"_L1].isString()) {
0225         if (m_parent->identityCached(obj["in_reply_to_account_id"_L1].toString())) {
0226             m_replyIdentity = m_parent->identityLookup(obj["in_reply_to_account_id"_L1].toString(), {});
0227         } else {
0228             const auto accountId = obj["in_reply_to_account_id"_L1].toString();
0229             QUrl uriAccount(m_parent->instanceUri());
0230             uriAccount.setPath(QStringLiteral("/api/v1/accounts/%1").arg(accountId));
0231 
0232             m_parent->get(uriAccount, true, this, [this, accountId](QNetworkReply *reply) {
0233                 const auto data = reply->readAll();
0234                 const auto doc = QJsonDocument::fromJson(data);
0235 
0236                 m_replyIdentity = m_parent->identityLookup(accountId, doc.object());
0237                 Q_EMIT replyIdentityChanged();
0238             });
0239         }
0240     } else if (!m_replyTargetId.isEmpty()) {
0241         // Fallback to getting the account id from the status, which is weird but this sometimes has to happen.
0242         m_parent->get(m_parent->apiUrl(QStringLiteral("/api/v1/statuses/%1").arg(m_replyTargetId)), true, this, [this](QNetworkReply *reply) {
0243             const auto data = reply->readAll();
0244             const auto doc = QJsonDocument::fromJson(data);
0245 
0246             m_replyIdentity = m_parent->identityLookup(doc["account"_L1].toObject()["id"_L1].toString(), doc["account"_L1].toObject());
0247             Q_EMIT replyIdentityChanged();
0248         });
0249     }
0250 
0251     m_url = QUrl(obj["url"_L1].toString());
0252 
0253     m_favouritesCount = obj["favourites_count"_L1].toInt();
0254     m_reblogsCount = obj["reblogs_count"_L1].toInt();
0255     m_repliesCount = obj["replies_count"_L1].toInt();
0256 
0257     m_favourited = obj["favourited"_L1].toBool();
0258     m_reblogged = obj["reblogged"_L1].toBool();
0259     m_bookmarked = obj["bookmarked"_L1].toBool();
0260     m_pinned = obj["pinned"_L1].toBool();
0261     m_muted = obj["muted"_L1].toBool();
0262 
0263     m_filters.clear();
0264 
0265     const auto filters = obj["filtered"_L1].toArray();
0266     for (const auto &filter : filters) {
0267         const auto filterContext = filter.toObject();
0268         const auto filterObj = filterContext["filter"_L1].toObject();
0269         m_filters << filterObj["title"_L1].toString();
0270 
0271         const auto filterAction = filterObj["filter_action"_L1];
0272         if (filterAction == "warn"_L1) {
0273             m_filtered = true;
0274         } else if (filterAction == "hide"_L1) {
0275             m_hidden = true;
0276         }
0277     }
0278 
0279     m_sensitive = obj["sensitive"_L1].toBool();
0280     m_visibility = stringToVisibility(obj["visibility"_L1].toString());
0281     m_language = obj["language"_L1].toString();
0282 
0283     m_publishedAt = QDateTime::fromString(obj["created_at"_L1].toString(), Qt::ISODate).toLocalTime();
0284 
0285     if (!obj["edited_at"_L1].isNull()) {
0286         m_editedAt = QDateTime::fromString(obj["edited_at"_L1].toString(), Qt::ISODate).toLocalTime();
0287     }
0288 
0289     m_attachments.clear();
0290     addAttachments(obj["media_attachments"_L1].toArray());
0291     const QJsonArray mentions = obj["mentions"_L1].toArray();
0292     if (obj.contains("card"_L1) && !obj["card"_L1].toObject().empty()) {
0293         setCard(std::make_optional<Card>(obj["card"_L1].toObject()));
0294     }
0295 
0296     if (obj.contains("application"_L1) && !obj["application"_L1].toObject().empty()) {
0297         setApplication(std::make_optional<Application>(obj["application"_L1].toObject()));
0298     }
0299 
0300     m_mentions.clear();
0301     for (const auto &m : std::as_const(mentions)) {
0302         const QJsonObject o = m.toObject();
0303         m_mentions.push_back(QStringLiteral("@") + o["acct"_L1].toString());
0304     }
0305 
0306     if (obj.contains(QStringLiteral("poll")) && !obj[QStringLiteral("poll")].isNull()) {
0307         m_poll = std::make_unique<Poll>(obj[QStringLiteral("poll")].toObject());
0308     }
0309 
0310     m_attachments_visible = !m_sensitive;
0311 }
0312 
0313 void Post::addAttachments(const QJsonArray &attachments)
0314 {
0315     for (const auto &attachment : attachments) {
0316         m_attachments.append(new Attachment{attachment.toObject(), this});
0317     }
0318 }
0319 
0320 void Post::addAttachment(const QJsonObject &attachment)
0321 {
0322     auto att = new Attachment{attachment, this};
0323     if (att->m_url.isEmpty()) {
0324         return;
0325     }
0326     m_attachments.append(att);
0327 
0328     Q_EMIT attachmentUploaded();
0329 }
0330 
0331 void Post::setInReplyTo(const QString &inReplyTo)
0332 {
0333     if (inReplyTo == m_replyTargetId) {
0334         return;
0335     }
0336     m_replyTargetId = inReplyTo;
0337     Q_EMIT inReplyToChanged();
0338 }
0339 
0340 int Post::repliesCount() const
0341 {
0342     return m_repliesCount;
0343 }
0344 
0345 QString Post::inReplyTo() const
0346 {
0347     return m_replyTargetId;
0348 }
0349 
0350 void Post::setDirtyAttachment()
0351 {
0352     m_parent->invalidatePost(this);
0353 }
0354 
0355 QStringList Post::mentions() const
0356 {
0357     return m_mentions;
0358 }
0359 
0360 QStringList Post::filters() const
0361 {
0362     return m_filters;
0363 }
0364 
0365 QUrl Post::url() const
0366 {
0367     return m_url;
0368 }
0369 
0370 void Post::setMentions(const QStringList &mentions)
0371 {
0372     if (mentions == m_mentions) {
0373         return;
0374     }
0375     m_mentions = mentions;
0376     Q_EMIT mentionsChanged();
0377 }
0378 
0379 QDateTime Post::publishedAt() const
0380 {
0381     return m_publishedAt;
0382 }
0383 
0384 QString Post::relativeTime() const
0385 {
0386     const auto current = QDateTime::currentDateTime();
0387     const auto publishingDate = publishedAt();
0388     const auto secsTo = publishingDate.secsTo(current);
0389     const auto daysTo = publishingDate.daysTo(current);
0390     if (secsTo < 0) {
0391         return i18n("in the future");
0392     } else if (secsTo < 60) {
0393         return i18n("%1s", qCeil(secsTo));
0394     } else if (secsTo < 60 * 60) {
0395         return i18n("%1m", qCeil(secsTo / 60));
0396     } else if (secsTo < 60 * 60 * 24) {
0397         return i18n("%1h", qCeil(secsTo / (60 * 60)));
0398     } else if (daysTo < 7) {
0399         return i18n("%1d", qCeil(daysTo));
0400     } else if (daysTo < 365) {
0401         const auto weeksTo = qCeil(daysTo / 7);
0402         if (weeksTo < 5) {
0403             return i18np("1 week ago", "%1 weeks ago", weeksTo);
0404         } else {
0405             const auto monthsTo = qCeil(daysTo / 30);
0406             return i18np("1 month ago", "%1 months ago", monthsTo);
0407         }
0408     } else {
0409         const auto yearsTo = qCeil(daysTo / 365);
0410         return i18np("1 year ago", "%1 years ago", yearsTo);
0411     }
0412 }
0413 
0414 QString Post::absoluteTime() const
0415 {
0416     return QLocale::system().toString(publishedAt(), QLocale::LongFormat);
0417 }
0418 
0419 QString Post::editedAt() const
0420 {
0421     return QLocale::system().toString(m_editedAt, QLocale::ShortFormat);
0422 }
0423 
0424 bool Post::wasEdited() const
0425 {
0426     return m_editedAt.isValid();
0427 }
0428 
0429 int Post::favouritesCount() const
0430 {
0431     return m_favouritesCount;
0432 }
0433 
0434 int Post::reblogsCount() const
0435 {
0436     return m_reblogsCount;
0437 }
0438 
0439 static QMap<QString, Notification::Type> str_to_not_type = {
0440     {QStringLiteral("favourite"), Notification::Type::Favorite},
0441     {QStringLiteral("follow"), Notification::Type::Follow},
0442     {QStringLiteral("mention"), Notification::Type::Mention},
0443     {QStringLiteral("reblog"), Notification::Type::Repeat},
0444     {QStringLiteral("update"), Notification::Type::Update},
0445     {QStringLiteral("poll"), Notification::Type::Poll},
0446     {QStringLiteral("status"), Notification::Type::Status},
0447     {QStringLiteral("follow_request"), Notification::Type::FollowRequest},
0448 };
0449 
0450 Notification::Notification(AbstractAccount *account, const QJsonObject &obj, QObject *parent)
0451     : m_account(account)
0452 {
0453     const auto accountObj = obj["account"_L1].toObject();
0454     const auto status = obj["status"_L1].toObject();
0455     const auto accountId = accountObj["id"_L1].toString();
0456     const auto type = obj["type"_L1].toString();
0457 
0458     m_post = createPost(m_account, status, parent);
0459     m_identity = m_account->identityLookup(accountId, accountObj);
0460     m_type = str_to_not_type[type];
0461     m_id = obj["id"_L1].toString().toInt();
0462 }
0463 
0464 int Notification::id() const
0465 {
0466     return m_id;
0467 }
0468 
0469 AbstractAccount *Notification::account() const
0470 {
0471     return m_account;
0472 }
0473 
0474 Notification::Type Notification::type() const
0475 {
0476     return m_type;
0477 }
0478 
0479 Post *Notification::post() const
0480 {
0481     return m_post;
0482 }
0483 
0484 std::shared_ptr<Identity> Notification::identity() const
0485 {
0486     return m_identity;
0487 }
0488 
0489 QString Post::spoilerText() const
0490 {
0491     return m_spoilerText;
0492 }
0493 
0494 void Post::setSpoilerText(const QString &spoilerText)
0495 {
0496     if (spoilerText == m_spoilerText) {
0497         return;
0498     }
0499     m_spoilerText = spoilerText;
0500     Q_EMIT spoilerTextChanged();
0501 }
0502 
0503 QString Post::content() const
0504 {
0505     return m_content;
0506 }
0507 
0508 void Post::setContent(const QString &content)
0509 {
0510     if (content == m_content) {
0511         return;
0512     }
0513     m_content = content;
0514     Q_EMIT contentChanged();
0515 }
0516 
0517 QVector<QString> Post::standaloneTags() const
0518 {
0519     return m_standaloneTags;
0520 }
0521 
0522 QString Post::contentType() const
0523 {
0524     return m_content_type;
0525 }
0526 
0527 void Post::setContentType(const QString &contentType)
0528 {
0529     if (m_content_type == contentType) {
0530         return;
0531     }
0532     m_content_type = contentType;
0533     Q_EMIT contentTypeChanged();
0534 }
0535 
0536 bool Post::sensitive() const
0537 {
0538     return m_sensitive;
0539 }
0540 
0541 void Post::setSensitive(bool sensitive)
0542 {
0543     if (m_sensitive == sensitive) {
0544         return;
0545     }
0546     m_sensitive = sensitive;
0547     Q_EMIT sensitiveChanged();
0548 }
0549 
0550 Post::Visibility Post::visibility() const
0551 {
0552     return m_visibility;
0553 }
0554 
0555 void Post::setVisibility(Visibility visibility)
0556 {
0557     if (visibility == m_visibility) {
0558         return;
0559     }
0560     m_visibility = visibility;
0561     Q_EMIT visibilityChanged();
0562 }
0563 
0564 QString Post::language() const
0565 {
0566     return m_language;
0567 }
0568 
0569 void Post::setLanguage(const QString &language)
0570 {
0571     if (language == m_language) {
0572         return;
0573     }
0574     m_language = language;
0575     Q_EMIT languageChanged();
0576 }
0577 
0578 std::optional<Card> Post::card() const
0579 {
0580     return m_card;
0581 }
0582 
0583 Card *Post::getCard() const
0584 {
0585     if (m_card.has_value()) {
0586         return const_cast<Card *>(&m_card.value());
0587     } else {
0588         return nullptr;
0589     }
0590 }
0591 
0592 void Post::setCard(std::optional<Card> card)
0593 {
0594     m_card = card;
0595 }
0596 
0597 std::optional<Application> Post::application() const
0598 {
0599     return m_application;
0600 }
0601 
0602 void Post::setApplication(std::optional<Application> application)
0603 {
0604     m_application = application;
0605 }
0606 
0607 bool Post::favourited() const
0608 {
0609     return m_favourited;
0610 }
0611 
0612 void Post::setFavourited(bool favourited)
0613 {
0614     m_favourited = favourited;
0615 }
0616 
0617 bool Post::reblogged() const
0618 {
0619     return m_reblogged;
0620 }
0621 
0622 void Post::setReblogged(bool reblogged)
0623 {
0624     m_reblogged = reblogged;
0625 }
0626 
0627 bool Post::muted() const
0628 {
0629     return m_muted;
0630 }
0631 
0632 void Post::setMuted(bool muted)
0633 {
0634     m_muted = muted;
0635 }
0636 
0637 bool Post::bookmarked() const
0638 {
0639     return m_bookmarked;
0640 }
0641 
0642 void Post::setBookmarked(bool bookmarked)
0643 {
0644     m_bookmarked = bookmarked;
0645 }
0646 
0647 bool Post::pinned() const
0648 {
0649     return m_pinned;
0650 }
0651 
0652 void Post::setPinned(bool pinned)
0653 {
0654     m_pinned = pinned;
0655 }
0656 
0657 bool Post::filtered() const
0658 {
0659     return m_filtered;
0660 }
0661 
0662 QList<Attachment *> Post::attachments() const
0663 {
0664     return m_attachments;
0665 }
0666 
0667 QQmlListProperty<Attachment> Post::attachmentList() const
0668 {
0669     return m_attachmentList;
0670 }
0671 
0672 void Post::setAttachmentsVisible(bool attachmentsVisible)
0673 {
0674     m_attachments_visible = attachmentsVisible;
0675 }
0676 
0677 bool Post::attachmentsVisible() const
0678 {
0679     return m_attachments_visible;
0680 }
0681 
0682 bool Post::boosted() const
0683 {
0684     return m_boosted;
0685 }
0686 
0687 Card::Card(QJsonObject card)
0688     : m_card(card)
0689 {
0690 }
0691 
0692 QString Card::authorName() const
0693 {
0694     return m_card[QLatin1String("author_name")].toString();
0695 }
0696 
0697 QString Card::authorUrl() const
0698 {
0699     return m_card[QLatin1String("author_url")].toString();
0700 }
0701 
0702 QString Card::blurhash() const
0703 {
0704     return m_card[QLatin1String("blurhash")].toString();
0705 }
0706 
0707 QString Card::description() const
0708 {
0709     return m_card[QLatin1String("description")].toString();
0710 }
0711 
0712 QString Card::embedUrl() const
0713 {
0714     return m_card[QLatin1String("embed_url")].toString();
0715 }
0716 
0717 int Card::width() const
0718 {
0719     return m_card[QLatin1String("weight")].toInt();
0720 }
0721 
0722 int Card::height() const
0723 {
0724     return m_card[QLatin1String("height")].toInt();
0725 }
0726 
0727 QString Card::html() const
0728 {
0729     return m_card[QLatin1String("html")].toString();
0730 }
0731 
0732 QString Card::image() const
0733 {
0734     return m_card[QLatin1String("image")].toString();
0735 }
0736 
0737 QString Card::providerName() const
0738 {
0739     const auto providerName = m_card[QLatin1String("provider_name")].toString();
0740     if (!providerName.isEmpty()) {
0741         return providerName;
0742     }
0743     return url().host();
0744 }
0745 
0746 QString Card::providerUrl() const
0747 {
0748     return m_card[QLatin1String("provider_url")].toString();
0749 }
0750 
0751 QString Card::title() const
0752 {
0753     return m_card[QLatin1String("title")].toString().trimmed();
0754 }
0755 
0756 QUrl Card::url() const
0757 {
0758     return QUrl::fromUserInput(m_card[QLatin1String("url")].toString());
0759 }
0760 
0761 Application::Application(QJsonObject application)
0762     : m_application(application)
0763 {
0764 }
0765 
0766 QString Application::name() const
0767 {
0768     return m_application[QLatin1String("name")].toString();
0769 }
0770 
0771 QUrl Application::website() const
0772 {
0773     return QUrl::fromUserInput(m_application[QLatin1String("website")].toString());
0774 }
0775 
0776 Identity *Post::getAuthorIdentity() const
0777 {
0778     return authorIdentity().get();
0779 }
0780 
0781 std::shared_ptr<Identity> Post::authorIdentity() const
0782 {
0783     return m_authorIdentity;
0784 }
0785 
0786 std::shared_ptr<Identity> Post::boostIdentity() const
0787 {
0788     return m_boostIdentity;
0789 }
0790 
0791 std::shared_ptr<Identity> Post::replyIdentity() const
0792 {
0793     return m_replyIdentity;
0794 }
0795 
0796 Poll *Post::poll() const
0797 {
0798     return m_poll.get();
0799 }
0800 
0801 void Post::setPollJson(const QJsonObject &object)
0802 {
0803     m_poll = std::make_unique<Poll>(object);
0804     Q_EMIT pollChanged();
0805 }
0806 
0807 QString Post::postId() const
0808 {
0809     return m_postId;
0810 }
0811 
0812 QString Post::originalPostId() const
0813 {
0814     return m_originalPostId;
0815 }
0816 
0817 bool Post::isEmpty() const
0818 {
0819     return m_postId.isEmpty();
0820 }
0821 
0822 QPair<QString, QList<QString>> Post::parseContent(const QString &html)
0823 {
0824     const QRegularExpression hashtagExp(QStringLiteral("(?:<a\\b[^>]*>#<span>(\\S*)<\\/span><\\/a>)"));
0825     const QRegularExpression extraneousParagraph(QStringLiteral("(\\s*(?:<(?:p|br)\\s*\\/?>)+\\s*<\\/p>)"));
0826 
0827     QList<QString> standaloneTags;
0828 
0829     // Find the last <p> or <br>
0830     const int lastBreak = html.lastIndexOf(QStringLiteral("<br>"));
0831     int lastParagraphBegin = html.lastIndexOf(QStringLiteral("<p>"));
0832     if (lastBreak > lastParagraphBegin) {
0833         lastParagraphBegin = lastBreak;
0834     }
0835 
0836     const int lastParagraphEnd = html.lastIndexOf(QStringLiteral("</p>"));
0837     QString lastParagraph = html.mid(lastParagraphBegin, lastParagraphEnd - html.length());
0838 
0839     QString processedHtml = html;
0840 
0841     // Catch all the tags in the last paragraph of the post, but only if they are not surrounded by text
0842     {
0843         QList<QString> possibleTags;
0844         QString possibleLastParagraph = lastParagraph;
0845 
0846         auto matchIterator = hashtagExp.globalMatch(possibleLastParagraph);
0847         while (matchIterator.hasNext()) {
0848             const QRegularExpressionMatch match = matchIterator.next();
0849             possibleTags.push_back(match.captured(1));
0850             possibleLastParagraph = possibleLastParagraph.replace(match.captured(0), QStringLiteral(""));
0851         }
0852 
0853         // If this paragraph is truly extraneous, then we can take its tags, otherwise skip.
0854         auto extraneousIterator = extraneousParagraph.globalMatch(possibleLastParagraph);
0855         if (extraneousIterator.hasNext()) {
0856             processedHtml.replace(lastParagraph, possibleLastParagraph);
0857             standaloneTags = possibleTags;
0858         }
0859     }
0860 
0861     const QRegularExpression extraneousBreakExp(QStringLiteral("(\\s*(?:<br\\s*\\/?>)+\\s*)<\\/p>"));
0862 
0863     // Ensure we remove any remaining <br>'s which will mess up the spacing in a post.
0864     // Example: "<p>Yosemite Valley reflections with rock<br />    </p>"
0865     {
0866         auto matchIterator = extraneousBreakExp.globalMatch(processedHtml);
0867         while (matchIterator.hasNext()) {
0868             const QRegularExpressionMatch match = matchIterator.next();
0869             processedHtml = processedHtml.replace(match.captured(1), QStringLiteral(""));
0870         }
0871     }
0872 
0873     // Ensure we remove any empty <p>'s which will mess up the spacing in a post.
0874     // Example: "<p>Boris Karloff (again) as Imhotep</p><p>  </p>"
0875     {
0876         auto matchIterator = extraneousParagraph.globalMatch(processedHtml);
0877         while (matchIterator.hasNext()) {
0878             const QRegularExpressionMatch match = matchIterator.next();
0879             processedHtml = processedHtml.replace(match.captured(1), QStringLiteral(""));
0880         }
0881     }
0882 
0883     return {processedHtml, standaloneTags};
0884 }
0885 
0886 #include "moc_post.cpp"