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 }