From bf73a19e11e5cccd22f58a5c97e1346dab5587c8 Mon Sep 17 00:00:00 2001 From: ConfuSomu Date: Thu, 3 Aug 2023 17:23:33 +0200 Subject: Create an Archive base class This class is inherited by MastodonArchive to provide Mastodon (and compatible) archive reading support. A base Archive class allows implementing reading support of other archive formats that are from other services. --- CMakeLists.txt | 6 +- src/archive/base_archive.cpp | 13 ++ src/archive/base_archive.h | 36 ++++ src/archive/mastodon.cpp | 390 ++++++++++++++++++++++++++++++++++++++++++ src/archive/mastodon.h | 40 +++++ src/archive_parser.cpp | 391 ------------------------------------------- src/archive_parser.h | 56 ------- src/list_item.h | 2 +- src/mainwindow.cpp | 6 +- src/mainwindow.h | 2 +- 10 files changed, 489 insertions(+), 453 deletions(-) create mode 100644 src/archive/base_archive.cpp create mode 100644 src/archive/base_archive.h create mode 100644 src/archive/mastodon.cpp create mode 100644 src/archive/mastodon.h delete mode 100644 src/archive_parser.cpp delete mode 100644 src/archive_parser.h diff --git a/CMakeLists.txt b/CMakeLists.txt index d28f320..2d09544 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,8 +24,10 @@ set(PROJECT_SOURCES src/finddialog.cpp src/finddialog.h src/finddialog.ui - src/archive_parser.cpp - src/archive_parser.h + src/archive/base_archive.cpp + src/archive/base_archive.h + src/archive/mastodon.cpp + src/archive/mastodon.h src/types.h src/list_item.cpp src/list_item.h diff --git a/src/archive/base_archive.cpp b/src/archive/base_archive.cpp new file mode 100644 index 0000000..14fa417 --- /dev/null +++ b/src/archive/base_archive.cpp @@ -0,0 +1,13 @@ +#include "src/archive/base_archive.h" +#include "src/archive/mastodon.h" + +Archive* Archive::create_archive(ArchiveType archive_type, const QString& main_filename) { + switch (archive_type) { + case MASTODON: + return new MastodonArchive(main_filename); + default: + return nullptr; + } +} + +Archive::Archive(const QString& filename) : main_filename(filename) {} diff --git a/src/archive/base_archive.h b/src/archive/base_archive.h new file mode 100644 index 0000000..fcabcf1 --- /dev/null +++ b/src/archive/base_archive.h @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include "src/types.h" + +enum ArchiveType { + MASTODON +}; + +class Archive { +public: + enum InitError { + NoError = 0, + FailedOpeningFile, + JsonParseError, + JsonEmpty, + JsonNull, + JsonNotObject, + JsonNotActivityStream // for ActivityPub archives + }; + + virtual ~Archive() {}; + virtual std::variant init() = 0; + + static Archive* create_archive(ArchiveType archive_type, const QString& main_filename); + + virtual void update_status_list(ViewStatusTypes allowed_types, QListWidget *parent) = 0; + virtual const QString get_html_status_info(int status_index, int text_zone_width, StatusType status_type, QLocale* locale) = 0; + virtual const QString get_html_status_text(int status_index) = 0; + +protected: + Archive(const QString& main_filename); + QString main_filename; +}; diff --git a/src/archive/mastodon.cpp b/src/archive/mastodon.cpp new file mode 100644 index 0000000..297383e --- /dev/null +++ b/src/archive/mastodon.cpp @@ -0,0 +1,390 @@ +#include "mastodon.h" +#include "src/list_item.h" +#include "src/types.h" +#include "src/activitypub/apactivity.h" +#include "src/activitypub/apobject.h" +#include "src/activitypub/apreblog.h" +#include "src/activitypub/appost.h" +#include "src/activitypub/apquestion.h" +#include "src/activitypub/fields.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +MastodonArchive::MastodonArchive(const QString& filename) : Archive(filename) {} + +std::variant MastodonArchive::init() { + QFile outbox_file(main_filename); + + if (!outbox_file.open(QIODevice::ReadOnly | QIODevice::Text)) + return FailedOpeningFile; + + QJsonParseError json_error; + QJsonDocument outbox_json_document = QJsonDocument::fromJson(outbox_file.readAll(), &json_error); + outbox_file.close(); + + if (json_error.error != QJsonParseError::NoError) + return JsonParseError; + if (outbox_json_document.isEmpty()) + return JsonEmpty; + if (outbox_json_document.isNull()) + return JsonNull; + + if (outbox_json_document.isObject()) + outbox_json = new QJsonObject(outbox_json_document.object()); + else + return JsonNotObject; + + // Do some more throughful checks to make sure that the JSON is actually valid and is a Mastodon data export (the only type supported currently) + if (not json_check_item(outbox_json->value("@context"), "https://www.w3.org/ns/activitystreams")) + return JsonNotActivityStream; + if (outbox_json->value("orderedItems").isArray()) { + outbox_items = new QJsonArray(outbox_json->value("orderedItems").toArray()); // we'll need it during Archive's lifetime + } else + return JsonParseError; + + archive_root_dir = QFileInfo(main_filename).absoluteDir(); + + return NoError; +} + +MastodonArchive::~MastodonArchive() { + delete outbox_json; outbox_json = nullptr; + delete outbox_items; outbox_items = nullptr; +} + +bool MastodonArchive::is_status_type_allowed(StatusType status_type, ViewStatusTypes allowed_types) { + switch (status_type) { + case PUBLIC: return allowed_types.includePublic; + case UNLISTED: return allowed_types.includeUnlisted; + case PRIVATE: return allowed_types.includePrivate; + case DIRECT: return allowed_types.includeDirect; + case REBLOG: return allowed_types.includeReblogs; + default: return true; + } +} + +// specific to Mastodon ActivityStreams archives +StatusType MastodonArchive::get_status_type(QJsonObject obj) { + /* + * public: + * to: #Public + * unlisted: + * to: /followers + * cc: #Public + * followers: + * to: /followers + * direct: + * to: the Actors mentioned + */ + + StatusType status_type = UNKNOWN; + bool is_private_or_unlisted = false; + + QJsonArray to; + + if (obj.value("to").isArray()) { + + to = obj.value("to").toArray(); + + // see https://www.w3.org/TR/activitypub/#public-addressing + for (auto collection : to) { + QString col = collection.toString(); + if (col == "https://www.w3.org/ns/activitystreams#Public") { + return PUBLIC; // status' privacy can only be promoted to a higher level + } + else if (col.endsWith("/followers")) // at least for Mastodon… + is_private_or_unlisted = true; // private or unlisted + } + } + + if (obj.value("cc").isArray()) { + QJsonArray cc = obj.value("cc").toArray(); + + for (auto collection : cc) { + QString col = collection.toString(); + if (col == "https://www.w3.org/ns/activitystreams#Public" and is_private_or_unlisted) { + return UNLISTED; // status' privacy can only be promoted to a higher level + } + } + if (is_private_or_unlisted) + return PRIVATE; + else if (to.size() > 0) + return DIRECT; + else if (to.size() == 0 and cc.size() == 0) // sending a direct message to no one or to another actor that doesn't exist anymore + return DIRECT; + } + + return status_type; +} + +// Note: No need to create an AP… class for that (unless we want to precache the Activities but that would be very slow and use a lot of memory) +void MastodonArchive::update_status_list(ViewStatusTypes allowed_types, QListWidget *parent) { + int i = 0; + for (auto&& item : *outbox_items) { + if (item.isObject()){ + QJsonObject obj = item.toObject(); + + if (not obj.value("type").isString()) // this shouldn't happen, but you never know + goto next_item; + + QString activity_type = obj.value("type").toString(); + if (not (activity_type == "Create" or activity_type == "Announce")) // ignoring everything that isn't a Create or Announce activity for now + goto next_item; + + // Determine the status' type + // NOTE: keep + StatusType status_type; + if (activity_type == "Announce") status_type = REBLOG; + else status_type = get_status_type(obj); + + if (not is_status_type_allowed(status_type, allowed_types)) + goto next_item; + + if (activity_type == "Create" and obj.value("object").isObject()) { + QJsonObject activity = obj.value("object").toObject(); + + bool has_attachment = false; + if (activity.contains("attachment") and not activity["attachment"].toArray().isEmpty()) + has_attachment = true; + + if (allowed_types.onlyWithAttachment and not has_attachment) + goto next_item; + + if (activity.value("content").isString()) { + // Strip HTML for display in list, according to https://stackoverflow.com/a/12157835 + QTextDocument strip_html; + strip_html.setHtml(activity.value("content").toString()); + + StatusListItem *item = new StatusListItem(strip_html.toPlainText(), status_type, has_attachment, this, parent, i); + } + } else if (activity_type == "Announce" and obj["object"].isString()) { + StatusListItem *item = new StatusListItem(activity_type, REBLOG, false, this, parent, i); + } + } + next_item: + ++i; + } +} + +QStringList MastodonArchive::get_status_object_list(QJsonObject obj, QString element_name) { + QStringList list; + + if (obj.contains(element_name)) { + QJsonArray elem = obj.value(element_name).toArray(); + + for (auto collection : elem) + list.append(collection.toString()); + } + + return list; +} + +QStringList MastodonArchive::get_status_object_language_list(QJsonObject obj, QString element_name = "contentMap") { + QStringList list; + + if (obj.contains(element_name) and obj.value(element_name).isObject()) { + QJsonObject content_map = obj.value(element_name).toObject(); + QStringList languages = content_map.keys(); + + for (auto lang : languages) + list.append(lang); + } + + return list; +} + +std::vector MastodonArchive::get_status_attachments_list(QJsonValueRef attachments_ref) { + std::vector list; + QJsonArray attachments = attachments_ref.toArray(); + + for (auto attachment_ref : attachments) { + APAttachmentFields element; + + QJsonObject attachment = attachment_ref.toObject(); + + if (attachment.contains("url")) { + QString url = attachment["url"].toString(); + + // NOTE: This will continue to be done by the archive parser when building the APAttachmentFields object!! SO DO NOT MOVE + // Initialize attachment_dir if it hasn't been done + if (attachment_dir_have_to_find) find_attachment_dir(url); + + // Also add a "/", after the attachment dir path, to make sure that the url starts with one as sometimes the ones in the json do not. We could make this prettier (but a bit slower) by only adding it conditionally. This might be worth it as the path can be copied and opened in the browser. + url.prepend(attachment_dir.absolutePath() + "/"); + + element.path = url; + element.filename = QFileInfo(url).fileName(); + } + + element.media_type = attachment["mediaType"].toString(); + element.name = attachment["name"].toString(); + + list.push_back(element); + } + + return list; +} + +// status_index is assumed to be a valid index +const QString MastodonArchive::get_html_status_info(int status_index, int text_zone_width, StatusType status_type, QLocale* locale) { + // the JSON AP Activity + QJsonObject activity = outbox_items->at(status_index).toObject(); + + APActivityFields act_fields = { + .visibility = status_type + }; + + QString obj_url; + + // The post + APObjectFields obj_fields; + APObjectType obj_type = APObjectType::UNKNOWN; + + if (activity.contains("type") and activity["type"].isString()) { + QString type = activity["type"].toString(); + if (type == "Create") act_fields.type = APActivityType::CREATE; + else if (type == "Announce") act_fields.type = APActivityType::ANNOUNCE; + else act_fields.type = APActivityType::UNKNOWN; + } + + if (activity.contains("published") and activity["published"].isString()) + act_fields.published = activity["published"].toString(); + + if (activity.contains("id") and activity["id"].isString()) + act_fields.object_url = activity["id"].toString(); + + if (activity.contains("actor")) + act_fields.by_actor = activity["actor"].toString(); // returns Null string if actor is not a string + + act_fields.to_actors = get_status_object_list(activity, "to"); + act_fields.cc_actors = get_status_object_list(activity, "cc"); + + if (activity["object"].isObject()) { + // the JSON AP Object + QJsonObject object = activity["object"].toObject(); + + { + QString type = object["type"].toString(); + if (type == "Note") + obj_type = APObjectType::NOTE; + else if (type == "Question") + obj_type = APObjectType::QUESTION; + } + + obj_fields.visibility = get_status_type(object); + + if (object.contains("summary")) + obj_fields.summary = object["summary"].toString(); + + obj_fields.attachments = get_status_attachments_list(object["attachment"]); + + obj_fields.web_url = object["url"].toString(); + obj_fields.object_url = object["atomUri"].toString(); // atomUri? + obj_fields.reply_to_url = object["inReplyTo"].toString(); + + if (object["content"].isString()) { + obj_fields.content = object["content"].toString(); + obj_fields.languages = get_status_object_language_list(object); + } + + obj_fields.to_actors = get_status_object_list(object, "to"); + obj_fields.cc_actors = get_status_object_list(object, "cc"); + obj_fields.by_actor = object["attributedTo"].toString(); + obj_fields.published = object["published"].toString(); + + if (obj_type == APObjectType::QUESTION) { + obj_fields.question = { + .end_time = object["endTime"].toString(), + .closed_time = object["closed"].toString(), + .total_votes = object["votersCount"].toInt() + }; + for (QJsonValue elem : object["oneOf"].toArray()) + obj_fields.question.poll_options.push_back({ + (elem["type"].toString() == "Note") ? elem["name"].toString() : "?", + elem["replies"].toObject()["totalItems"].toInt() + }); + } + + } else if (activity["object"].isString()) + obj_url = activity["object"].toString(); + + if (not obj_url.isNull()) + act_fields.object = new APReblog({obj_url, act_fields.visibility}); + else switch(obj_type) { + case APObjectType::UNKNOWN: + case APObjectType::NOTE: + act_fields.object = new APPost(obj_fields); break; + case APObjectType::QUESTION: + act_fields.object = new APQuestion(obj_fields); break; + } + + // TODO: it is currently a waste to create this APActivity object that will be immediately destroyed but it allows us to extend archive parsing to something that will become abstract (and an abstract base class) and separate from display (the final goal) which will allow us to add other sources that feed us with posts, reblogs and Actor information. furthermore, these objects can be cached for reuse in a session. + return APActivity(act_fields).get_html_render({text_zone_width, locale}); +} + +// TODO: make this use an APActivity object that will be present as an StatusListItem member. +const QString MastodonArchive::get_html_status_text(int status_index) { + QString text(""); + QJsonObject obj = outbox_items->at(status_index).toObject(); + + if (obj["object"].isObject()) { + QJsonObject activity = obj["object"].toObject(); + + if (activity.contains("summary")) { + QString summary_text = activity["summary"].toString(); + if (not summary_text.isEmpty()) + text.append(QString("

CW: {{summary}}

").replace("{{summary}}", summary_text)); + } + + if (activity["content"].isString()) { + text.append(QString("
{{content}}
").replace("{{content}}", activity["content"].toString())); + } + } + + return text; +} + +void MastodonArchive::find_attachment_dir(QString example_attachment) { + // Find the root directory name of the attachment + QString root_name = example_attachment.split('/', Qt::SkipEmptyParts)[0]; + + // Iterate over each subdirectory of the archive to find the directory containing the attachments + archive_root_dir.setFilter(QDir::Dirs|QDir::NoDotAndDotDot); + QDirIterator it(archive_root_dir, QDirIterator::Subdirectories); + while (it.hasNext()) { + QString current_dir = it.next(); + + if (current_dir.section('/', -1) == root_name) { + // We have found the directory + // Remove the root_name component (that should be at the end) as it will be added back due to being part of the attachment url + attachment_dir.setPath(current_dir.remove(root_name)); + attachment_dir_have_to_find = false; + + if (not attachment_dir.exists()) + // This shouldn't happen, but log it in case + qDebug() << "Attachment dir does not exist!" << attachment_dir.canonicalPath(); + return; + } + } + // If the attachment directory wasn't found, it will be searched for next attachment url parsing as attachment_dir_have_to_find wasn't touched (and is still true) +} + +bool MastodonArchive::json_check_item(const QJsonValue& value, const QString& item) { + // This allows us to avoid having an "and" condition that has QJsonObject::contains() and simplifies the if statement to only having this current function + if (value.type() == QJsonValue::Undefined) return false; + + if (value.toString() == item) + return true; + else if (value.isArray()) { + QJsonArray array = value.toArray(); + return array.contains(item); + } else + return false; +} diff --git a/src/archive/mastodon.h b/src/archive/mastodon.h new file mode 100644 index 0000000..48be1f3 --- /dev/null +++ b/src/archive/mastodon.h @@ -0,0 +1,40 @@ +#pragma once + +#include "src/archive/base_archive.h" +#include "src/activitypub/fields.h" +#include "src/types.h" + +#include +#include +#include +#include +#include + +class MastodonArchive : public Archive { +public: + MastodonArchive(const QString& filename); + ~MastodonArchive(); + std::variant init(); + + void update_status_list(ViewStatusTypes allowed_types, QListWidget *parent); + const QString get_html_status_info(int status_index, int text_zone_width, StatusType status_type, QLocale* locale); + const QString get_html_status_text(int status_index); +private: + QDir archive_root_dir; + QDir attachment_dir; + bool attachment_dir_have_to_find = true; + + QJsonObject *outbox_json = nullptr; + QJsonArray *outbox_items = nullptr; + + bool is_status_type_allowed(StatusType status_type, ViewStatusTypes allowed_types); + StatusType get_status_type(QJsonObject obj); + QStringList get_status_object_list(QJsonObject obj, QString element_name) ; + QStringList get_status_object_language_list(QJsonObject obj, QString element_name); + std::vector get_status_attachments_list(QJsonValueRef attachments_ref); + void find_attachment_dir(QString example_attachment); + + // Check the `object`, which may be a simple string or a QJsonArray, for the existance of a string `item`. + // Returns true if the `item` exists, else false. + bool json_check_item(const QJsonValue& object, const QString& item); +}; diff --git a/src/archive_parser.cpp b/src/archive_parser.cpp deleted file mode 100644 index ab7f67d..0000000 --- a/src/archive_parser.cpp +++ /dev/null @@ -1,391 +0,0 @@ -#include "archive_parser.h" -#include "src/list_item.h" -#include "src/types.h" -#include "activitypub/apactivity.h" -#include "activitypub/apobject.h" -#include "activitypub/apreblog.h" -#include "activitypub/appost.h" -#include "activitypub/apquestion.h" -#include "activitypub/fields.h" - -#include -#include -#include -#include -#include -#include -#include -#include - -Archive::Archive(QString outbox_filename, ArchiveType archive_type) : - outbox_filename(outbox_filename), archive_type(archive_type) {} - -std::variant Archive::init() { - QFile outbox_file(outbox_filename); - - if (!outbox_file.open(QIODevice::ReadOnly | QIODevice::Text)) - return FailedOpeningFile; - - QJsonParseError json_error; - QJsonDocument outbox_json_document = QJsonDocument::fromJson(outbox_file.readAll(), &json_error); - outbox_file.close(); - - if (json_error.error != QJsonParseError::NoError) - return JsonParseError; - if (outbox_json_document.isEmpty()) - return JsonEmpty; - if (outbox_json_document.isNull()) - return JsonNull; - - if (outbox_json_document.isObject()) - outbox_json = new QJsonObject(outbox_json_document.object()); - else - return JsonNotObject; - - // Do some more throughful checks to make sure that the JSON is actually valid and is a Mastodon data export (the only type supported currently) - if (not json_check_item(outbox_json->value("@context"), "https://www.w3.org/ns/activitystreams")) - return JsonNotActivityStream; - if (outbox_json->value("orderedItems").isArray()) { - outbox_items = new QJsonArray(outbox_json->value("orderedItems").toArray()); // we'll need it during Archive's lifetime - } else - return JsonParseError; - - archive_root_dir = QFileInfo(outbox_filename).absoluteDir(); - - return NoError; -} - -Archive::~Archive() { - delete outbox_json; outbox_json = nullptr; - delete outbox_items; outbox_items = nullptr; -} - -bool Archive::is_status_type_allowed(StatusType status_type, ViewStatusTypes allowed_types) { - switch (status_type) { - case PUBLIC: return allowed_types.includePublic; - case UNLISTED: return allowed_types.includeUnlisted; - case PRIVATE: return allowed_types.includePrivate; - case DIRECT: return allowed_types.includeDirect; - case REBLOG: return allowed_types.includeReblogs; - default: return true; - } -} - -// specific to Mastodon ActivityStreams archives -StatusType Archive::get_status_type(QJsonObject obj) { - /* - * public: - * to: #Public - * unlisted: - * to: /followers - * cc: #Public - * followers: - * to: /followers - * direct: - * to: the Actors mentioned - */ - - StatusType status_type = UNKNOWN; - bool is_private_or_unlisted = false; - - QJsonArray to; - - if (obj.value("to").isArray()) { - - to = obj.value("to").toArray(); - - // see https://www.w3.org/TR/activitypub/#public-addressing - for (auto collection : to) { - QString col = collection.toString(); - if (col == "https://www.w3.org/ns/activitystreams#Public") { - return PUBLIC; // status' privacy can only be promoted to a higher level - } - else if (col.endsWith("/followers")) // at least for Mastodon… - is_private_or_unlisted = true; // private or unlisted - } - } - - if (obj.value("cc").isArray()) { - QJsonArray cc = obj.value("cc").toArray(); - - for (auto collection : cc) { - QString col = collection.toString(); - if (col == "https://www.w3.org/ns/activitystreams#Public" and is_private_or_unlisted) { - return UNLISTED; // status' privacy can only be promoted to a higher level - } - } - if (is_private_or_unlisted) - return PRIVATE; - else if (to.size() > 0) - return DIRECT; - else if (to.size() == 0 and cc.size() == 0) // sending a direct message to no one or to another actor that doesn't exist anymore - return DIRECT; - } - - return status_type; -} - -// Note: No need to create an AP… class for that (unless we want to precache the Activities but that would be very slow and use a lot of memory) -void Archive::update_status_list(ViewStatusTypes allowed_types, QListWidget *parent) { - int i = 0; - for (auto&& item : *outbox_items) { - if (item.isObject()){ - QJsonObject obj = item.toObject(); - - if (not obj.value("type").isString()) // this shouldn't happen, but you never know - goto next_item; - - QString activity_type = obj.value("type").toString(); - if (not (activity_type == "Create" or activity_type == "Announce")) // ignoring everything that isn't a Create or Announce activity for now - goto next_item; - - // Determine the status' type - // NOTE: keep - StatusType status_type; - if (activity_type == "Announce") status_type = REBLOG; - else status_type = get_status_type(obj); - - if (not is_status_type_allowed(status_type, allowed_types)) - goto next_item; - - if (activity_type == "Create" and obj.value("object").isObject()) { - QJsonObject activity = obj.value("object").toObject(); - - bool has_attachment = false; - if (activity.contains("attachment") and not activity["attachment"].toArray().isEmpty()) - has_attachment = true; - - if (allowed_types.onlyWithAttachment and not has_attachment) - goto next_item; - - if (activity.value("content").isString()) { - // Strip HTML for display in list, according to https://stackoverflow.com/a/12157835 - QTextDocument strip_html; - strip_html.setHtml(activity.value("content").toString()); - - StatusListItem *item = new StatusListItem(strip_html.toPlainText(), status_type, has_attachment, this, parent, i); - } - } else if (activity_type == "Announce" and obj["object"].isString()) { - StatusListItem *item = new StatusListItem(activity_type, REBLOG, false, this, parent, i); - } - } - next_item: - ++i; - } -} - -QStringList Archive::get_status_object_list(QJsonObject obj, QString element_name) { - QStringList list; - - if (obj.contains(element_name)) { - QJsonArray elem = obj.value(element_name).toArray(); - - for (auto collection : elem) - list.append(collection.toString()); - } - - return list; -} - -QStringList Archive::get_status_object_language_list(QJsonObject obj, QString element_name = "contentMap") { - QStringList list; - - if (obj.contains(element_name) and obj.value(element_name).isObject()) { - QJsonObject content_map = obj.value(element_name).toObject(); - QStringList languages = content_map.keys(); - - for (auto lang : languages) - list.append(lang); - } - - return list; -} - -std::vector Archive::get_status_attachments_list(QJsonValueRef attachments_ref) { - std::vector list; - QJsonArray attachments = attachments_ref.toArray(); - - for (auto attachment_ref : attachments) { - APAttachmentFields element; - - QJsonObject attachment = attachment_ref.toObject(); - - if (attachment.contains("url")) { - QString url = attachment["url"].toString(); - - // NOTE: This will continue to be done by the archive parser when building the APAttachmentFields object!! SO DO NOT MOVE - // Initialize attachment_dir if it hasn't been done - if (attachment_dir_have_to_find) find_attachment_dir(url); - - // Also add a "/", after the attachment dir path, to make sure that the url starts with one as sometimes the ones in the json do not. We could make this prettier (but a bit slower) by only adding it conditionally. This might be worth it as the path can be copied and opened in the browser. - url.prepend(attachment_dir.absolutePath() + "/"); - - element.path = url; - element.filename = QFileInfo(url).fileName(); - } - - element.media_type = attachment["mediaType"].toString(); - element.name = attachment["name"].toString(); - - list.push_back(element); - } - - return list; -} - -// status_index is assumed to be a valid index -const QString Archive::get_html_status_info(int status_index, int text_zone_width, StatusType status_type, QLocale* locale) { - // the JSON AP Activity - QJsonObject activity = outbox_items->at(status_index).toObject(); - - APActivityFields act_fields = { - .visibility = status_type - }; - - QString obj_url; - - // The post - APObjectFields obj_fields; - APObjectType obj_type = APObjectType::UNKNOWN; - - if (activity.contains("type") and activity["type"].isString()) { - QString type = activity["type"].toString(); - if (type == "Create") act_fields.type = APActivityType::CREATE; - else if (type == "Announce") act_fields.type = APActivityType::ANNOUNCE; - else act_fields.type = APActivityType::UNKNOWN; - } - - if (activity.contains("published") and activity["published"].isString()) - act_fields.published = activity["published"].toString(); - - if (activity.contains("id") and activity["id"].isString()) - act_fields.object_url = activity["id"].toString(); - - if (activity.contains("actor")) - act_fields.by_actor = activity["actor"].toString(); // returns Null string if actor is not a string - - act_fields.to_actors = get_status_object_list(activity, "to"); - act_fields.cc_actors = get_status_object_list(activity, "cc"); - - if (activity["object"].isObject()) { - // the JSON AP Object - QJsonObject object = activity["object"].toObject(); - - { - QString type = object["type"].toString(); - if (type == "Note") - obj_type = APObjectType::NOTE; - else if (type == "Question") - obj_type = APObjectType::QUESTION; - } - - obj_fields.visibility = get_status_type(object); - - if (object.contains("summary")) - obj_fields.summary = object["summary"].toString(); - - obj_fields.attachments = get_status_attachments_list(object["attachment"]); - - obj_fields.web_url = object["url"].toString(); - obj_fields.object_url = object["atomUri"].toString(); // atomUri? - obj_fields.reply_to_url = object["inReplyTo"].toString(); - - if (object["content"].isString()) { - obj_fields.content = object["content"].toString(); - obj_fields.languages = get_status_object_language_list(object); - } - - obj_fields.to_actors = get_status_object_list(object, "to"); - obj_fields.cc_actors = get_status_object_list(object, "cc"); - obj_fields.by_actor = object["attributedTo"].toString(); - obj_fields.published = object["published"].toString(); - - if (obj_type == APObjectType::QUESTION) { - obj_fields.question = { - .end_time = object["endTime"].toString(), - .closed_time = object["closed"].toString(), - .total_votes = object["votersCount"].toInt() - }; - for (QJsonValue elem : object["oneOf"].toArray()) - obj_fields.question.poll_options.push_back({ - (elem["type"].toString() == "Note") ? elem["name"].toString() : "?", - elem["replies"].toObject()["totalItems"].toInt() - }); - } - - } else if (activity["object"].isString()) - obj_url = activity["object"].toString(); - - if (not obj_url.isNull()) - act_fields.object = new APReblog({obj_url, act_fields.visibility}); - else switch(obj_type) { - case APObjectType::UNKNOWN: - case APObjectType::NOTE: - act_fields.object = new APPost(obj_fields); break; - case APObjectType::QUESTION: - act_fields.object = new APQuestion(obj_fields); break; - } - - // TODO: it is currently a waste to create this APActivity object that will be immediately destroyed but it allows us to extend archive parsing to something that will become abstract (and an abstract base class) and separate from display (the final goal) which will allow us to add other sources that feed us with posts, reblogs and Actor information. furthermore, these objects can be cached for reuse in a session. - return APActivity(act_fields).get_html_render({text_zone_width, locale}); -} - -// TODO: make this use an APActivity object that will be present as an StatusListItem member. -const QString Archive::get_html_status_text(int status_index) { - QString text(""); - QJsonObject obj = outbox_items->at(status_index).toObject(); - - if (obj["object"].isObject()) { - QJsonObject activity = obj["object"].toObject(); - - if (activity.contains("summary")) { - QString summary_text = activity["summary"].toString(); - if (not summary_text.isEmpty()) - text.append(QString("

CW: {{summary}}

").replace("{{summary}}", summary_text)); - } - - if (activity["content"].isString()) { - text.append(QString("
{{content}}
").replace("{{content}}", activity["content"].toString())); - } - } - - return text; -} - -void Archive::find_attachment_dir(QString example_attachment) { - // Find the root directory name of the attachment - QString root_name = example_attachment.split('/', Qt::SkipEmptyParts)[0]; - - // Iterate over each subdirectory of the archive to find the directory containing the attachments - archive_root_dir.setFilter(QDir::Dirs|QDir::NoDotAndDotDot); - QDirIterator it(archive_root_dir, QDirIterator::Subdirectories); - while (it.hasNext()) { - QString current_dir = it.next(); - - if (current_dir.section('/', -1) == root_name) { - // We have found the directory - // Remove the root_name component (that should be at the end) as it will be added back due to being part of the attachment url - attachment_dir.setPath(current_dir.remove(root_name)); - attachment_dir_have_to_find = false; - - if (not attachment_dir.exists()) - // This shouldn't happen, but log it in case - qDebug() << "Attachment dir does not exist!" << attachment_dir.canonicalPath(); - return; - } - } - // If the attachment directory wasn't found, it will be searched for next attachment url parsing as attachment_dir_have_to_find wasn't touched (and is still true) -} - -bool Archive::json_check_item(const QJsonValue& value, const QString& item) { - // This allows us to avoid having an "and" condition that has QJsonObject::contains() and simplifies the if statement to only having this current function - if (value.type() == QJsonValue::Undefined) return false; - - if (value.toString() == item) - return true; - else if (value.isArray()) { - QJsonArray array = value.toArray(); - return array.contains(item); - } else - return false; -} diff --git a/src/archive_parser.h b/src/archive_parser.h deleted file mode 100644 index 162787e..0000000 --- a/src/archive_parser.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -#include "types.h" -#include "activitypub/fields.h" - -enum ArchiveType { - MASTODON -}; - -class Archive { -public: - enum InitError { - NoError = 0, - FailedOpeningFile, - JsonParseError, - JsonEmpty, - JsonNull, - JsonNotObject, - JsonNotActivityStream // for ActivityPub archives - }; - - Archive(QString outbox_filename, ArchiveType archive_type); - ~Archive(); - std::variant init(); - - void update_status_list(ViewStatusTypes allowed_types, QListWidget *parent); - const QString get_html_status_info(int status_index, int text_zone_width, StatusType status_type, QLocale* locale); - const QString get_html_status_text(int status_index); -private: - QString outbox_filename; - QDir archive_root_dir; - ArchiveType archive_type; - QDir attachment_dir; - bool attachment_dir_have_to_find = true; - - QJsonObject *outbox_json = nullptr; - QJsonArray *outbox_items = nullptr; - - bool is_status_type_allowed(StatusType status_type, ViewStatusTypes allowed_types); - StatusType get_status_type(QJsonObject obj); - QStringList get_status_object_list(QJsonObject obj, QString element_name) ; - QStringList get_status_object_language_list(QJsonObject obj, QString element_name); - std::vector get_status_attachments_list(QJsonValueRef attachments_ref); - void find_attachment_dir(QString example_attachment); - - // Check the `object`, which may be a simple string or a QJsonArray, for the existance of a string `item`. - // Returns true if the `item` exists, else false. - bool json_check_item(const QJsonValue& object, const QString& item); -}; diff --git a/src/list_item.h b/src/list_item.h index 4514903..d730551 100644 --- a/src/list_item.h +++ b/src/list_item.h @@ -1,7 +1,7 @@ #pragma once #include -#include "src/archive_parser.h" +#include "src/archive/base_archive.h" #include "types.h" class StatusListItem : public QListWidgetItem { diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 89f550d..4fc3970 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -1,6 +1,6 @@ #include "mainwindow.h" #include "./ui_mainwindow.h" -#include "src/archive_parser.h" +#include "src/archive/base_archive.h" #include "src/finddialog.h" #include "src/list_item.h" #include "src/command_line.h" @@ -218,7 +218,9 @@ void MainWindow::open_file(const QString &filename) { open_file_filename = filename; QApplication::setOverrideCursor(Qt::WaitCursor); - data_archive = new Archive(filename, ArchiveType::MASTODON); + + data_archive = Archive::create_archive(ArchiveType::MASTODON, filename); + if (not data_archive) return; QFuture> parse_error = QtConcurrent::run(data_archive, &Archive::init); archive_thread_watcher.setFuture(parse_error); diff --git a/src/mainwindow.h b/src/mainwindow.h index 162e6bf..fced119 100644 --- a/src/mainwindow.h +++ b/src/mainwindow.h @@ -6,7 +6,7 @@ #include #include -#include "archive_parser.h" +#include "archive/base_archive.h" #include "types.h" #include "command_line.h" #include "finddialog.h" -- cgit v1.2.3-54-g00ecf