aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorConfuSomu2023-07-05 15:14:46 +0200
committerConfuSomu2023-07-05 15:14:46 +0200
commite515ca109faa897e84be8a97a1240c59df612dd4 (patch)
tree2a5e0593bb4387449a9a60fb2aefcc74779b5c67
parent0dfab2456b2dcd33f1879ac7d5606dd65cdb8e40 (diff)
downloadActorViewer-e515ca109faa897e84be8a97a1240c59df612dd4.tar
ActorViewer-e515ca109faa897e84be8a97a1240c59df612dd4.tar.gz
ActorViewer-e515ca109faa897e84be8a97a1240c59df612dd4.zip
Implement a small library of ActivityPub objects
These objects allow, and will allow us, to move HTML rendering out of the archive parser and into separate classes. Each of the derived classes specialise HTML rendering for their specific requirements. Furthermore, these ActivityPub objects will be able to be expanded upon and have support to be written to disk, in a database, for instance. Separating ActivityPub object retrieving from rendering allows us to implement other retrieving sources and methods, such as downloading posts from a configured remote instance.
-rw-r--r--CMakeLists.txt16
-rw-r--r--src/activitypub/apactivity.cpp46
-rw-r--r--src/activitypub/apactivity.h45
-rw-r--r--src/activitypub/apactor.cpp35
-rw-r--r--src/activitypub/apactor.h25
-rw-r--r--src/activitypub/apattachment.cpp45
-rw-r--r--src/activitypub/apattachment.h33
-rw-r--r--src/activitypub/apbase.cpp21
-rw-r--r--src/activitypub/apbase.h22
-rw-r--r--src/activitypub/apobject.cpp23
-rw-r--r--src/activitypub/apobject.h36
-rw-r--r--src/activitypub/appost.cpp71
-rw-r--r--src/activitypub/appost.h43
-rw-r--r--src/activitypub/apreblog.cpp13
-rw-r--r--src/activitypub/apreblog.h27
-rw-r--r--src/archive_parser.cpp176
-rw-r--r--src/archive_parser.h3
-rw-r--r--src/mainwindow.cpp1
-rw-r--r--src/templates/apactivity.html16
-rw-r--r--src/templates/apattachment.html3
-rw-r--r--src/templates/apattachmentlist_item.html3
-rw-r--r--src/templates/appost.html13
-rw-r--r--src/templates/apreblog.html4
23 files changed, 623 insertions, 97 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9c6381c..f0994a9 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -12,7 +12,7 @@ set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For Kate's LSP
-find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets)
+find_package(QT NAMES Qt5 REQUIRED COMPONENTS Widgets)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets)
# add_subdirectory will happen later…
@@ -28,6 +28,20 @@ set(PROJECT_SOURCES
src/list_item.h
src/command_line.cpp
src/command_line.h
+ src/activitypub/apactivity.h
+ src/activitypub/apactor.h
+ src/activitypub/apattachment.h
+ src/activitypub/apbase.h
+ src/activitypub/apobject.h
+ src/activitypub/appost.h
+ src/activitypub/apreblog.h
+ src/activitypub/apactivity.cpp
+ src/activitypub/apactor.cpp
+ src/activitypub/apattachment.cpp
+ src/activitypub/apbase.cpp
+ src/activitypub/apobject.cpp
+ src/activitypub/appost.cpp
+ src/activitypub/apreblog.cpp
)
if(${QT_VERSION_MAJOR} GREATER_EQUAL 6)
diff --git a/src/activitypub/apactivity.cpp b/src/activitypub/apactivity.cpp
new file mode 100644
index 0000000..5f1c36d
--- /dev/null
+++ b/src/activitypub/apactivity.cpp
@@ -0,0 +1,46 @@
+#include "apactivity.h"
+#include "apactor.h"
+
+#include <QDebug>
+
+APActivity::APActivity() {}
+
+APActivity::APActivity(APActivityFields fields) {
+ for (QString actor_url : fields.to_actors)
+ to_actors.push_back(APActor(actor_url));
+ for (QString actor_url : fields.cc_actors)
+ cc_actors.push_back(APActor(actor_url));
+ by_actor = fields.by_actor;
+
+ object_url = fields.object_url;
+ published = QDateTime::fromString(fields.published, Qt::ISODate);
+
+ visibility = fields.visibility;
+ type = fields.type;
+
+ object = fields.object;
+}
+
+APActivity::~APActivity() {
+ delete object;
+}
+
+QString APActivity::get_html_render(HtmlRenderDetails render_info) {
+ QString html(get_html_template("apactivity"));
+
+ switch (type) {
+ case APActivityType::CREATE:
+ html.replace("{{type}}", "Create"); break;
+ case APActivityType::ANNOUNCE:
+ html.replace("{{type}}", "Announce"); break;
+ default:
+ html.replace("{{type}}", "<i>Unknown</i>"); break;
+ }
+
+ html.replace("{{by}}", by_actor.get_html_render(render_info));
+ html.replace("{{object}}", object->get_html_render(render_info));
+ if (published.isValid())
+ html.replace("{{published}}", render_info.locale->toString(published.toLocalTime()));
+
+ return html;
+}
diff --git a/src/activitypub/apactivity.h b/src/activitypub/apactivity.h
new file mode 100644
index 0000000..fcfb86c
--- /dev/null
+++ b/src/activitypub/apactivity.h
@@ -0,0 +1,45 @@
+#pragma once
+
+#include "apactor.h"
+#include "apbase.h"
+#include "apobject.h"
+#include "../types.h"
+#include <QDateTime>
+
+enum struct APActivityType {CREATE, ANNOUNCE, UNKNOWN};
+
+struct APActivityFields {
+ QString by_actor;
+ QStringList to_actors;
+ QStringList cc_actors;
+ QString object_url; // maps to "id", represents the Activity, *not* the URL of the object it contains
+ QString published;
+ APObject* object; // will generally be a APPost or APReblog
+ StatusType visibility;
+ APActivityType type = APActivityType::UNKNOWN;
+};
+
+class APActivity : APBase {
+public:
+ APActivity();
+
+ // An Activity that can easily be constructed from an ActivityStreams JSON Activity. Make sure to pass the APObject.
+ APActivity(APActivityFields fields);
+ ~APActivity();
+
+ APObject* object = nullptr; // the object that's manipulated by this Activity, use APPost for something useful and "manually" create it
+
+ QString get_html_render(HtmlRenderDetails render_info);
+
+private:
+ APActorList to_actors;
+ APActorList cc_actors;
+ APActor by_actor; // the Actor who did this Activity, maps to "actor"
+
+ APActivityType type;
+ StatusType visibility;
+
+ //QString object_url; // activity's URL, maps to "id"
+
+ QDateTime published;
+};
diff --git a/src/activitypub/apactor.cpp b/src/activitypub/apactor.cpp
new file mode 100644
index 0000000..6731dd3
--- /dev/null
+++ b/src/activitypub/apactor.cpp
@@ -0,0 +1,35 @@
+#include "apactor.h"
+
+APActor::APActor() {}
+
+APActor::APActor(const QString url) {
+ object_url = url;
+}
+
+const QString APActor::get_url() {
+ return object_url;
+}
+
+QString APActor::get_html_render(HtmlRenderDetails render_info) {
+ // TODO: could be made nicer with profile images
+ return object_url;
+}
+
+const QString APActor::get_plain_render() {
+ return object_url;
+}
+
+QString APActorList::get_html_render(HtmlRenderDetails render_info) {
+ // have something basic for the moment
+ QString text;
+
+ for (APActor elem : *this)
+ text.append(elem.get_plain_render() + ", ");
+
+ if (text.isEmpty())
+ text = "<em>(empty)</em>";
+ else
+ text.chop(2); // remove leftover ", "
+
+ return text;
+}
diff --git a/src/activitypub/apactor.h b/src/activitypub/apactor.h
new file mode 100644
index 0000000..60ef0bf
--- /dev/null
+++ b/src/activitypub/apactor.h
@@ -0,0 +1,25 @@
+#pragma once
+
+#include "apbase.h"
+#include <vector>
+
+// APActors will have an avatar, display name, username and more that will be fetched using the remote instance
+class APActor : APBase {
+public:
+ // Empty actor
+ APActor();
+ APActor(const QString url);
+ ~APActor() {};
+
+ const QString get_url();
+
+ QString get_html_render(HtmlRenderDetails render_info);
+ const QString get_plain_render();
+private:
+ QString object_url;
+};
+
+class APActorList : public std::vector<APActor>, APBase {
+public:
+ QString get_html_render(HtmlRenderDetails render_info);
+};
diff --git a/src/activitypub/apattachment.cpp b/src/activitypub/apattachment.cpp
new file mode 100644
index 0000000..0589291
--- /dev/null
+++ b/src/activitypub/apattachment.cpp
@@ -0,0 +1,45 @@
+#include "apattachment.h"
+#include <QFileInfo>
+
+APAttachment::APAttachment() {}
+
+APAttachment::APAttachment(APAttachmentFields fields) {
+ path_url = fields.path;
+ type = fields.media_type;
+ description = fields.name;
+}
+
+QString APAttachment::get_html_render(HtmlRenderDetails info) {
+ QString html(get_html_template("apattachment"));
+
+ if (not path_url.isEmpty()) {
+ html.replace("{{path}}", path_url);
+ html.replace("{{filename}}", QFileInfo(path_url).fileName()); // FIXME: this is maybe ugly?
+ }
+
+ if (type.startsWith("image/"))
+ // dynamically resize image based on the display widget size to avoid horizontal scrolling
+ html.replace("{{img-width}}", QString::number((float)info.text_zone_width - (float)info.text_zone_width*0.15)); // finally after passing this for so long i finally use it
+ else
+ html.replace("{{img-width}}", "0");
+
+ html.replace("{{alt-text}}", description);
+
+ return html;
+}
+
+QString APAttachmentList::get_html_render(HtmlRenderDetails render_info) {
+ QString html;
+
+ int i = 1;
+ for (APAttachment attachment : *this) {
+ QString item_html(get_html_template("apattachmentlist_item"));
+ item_html.replace("{{id}}", QString::number(i));
+ item_html.replace("{{attachment}}", attachment.get_html_render(render_info));
+
+ html.append(item_html);
+ ++i;
+ }
+
+ return html;
+}
diff --git a/src/activitypub/apattachment.h b/src/activitypub/apattachment.h
new file mode 100644
index 0000000..c8cdda0
--- /dev/null
+++ b/src/activitypub/apattachment.h
@@ -0,0 +1,33 @@
+#pragma once
+
+#include "apbase.h"
+#include <array>
+#include <vector>
+
+struct APAttachmentFields {
+ QString path;
+ QString filename; // "nicer" filename
+ QString media_type;
+ QString name;
+};
+
+class APAttachment : APBase {
+public:
+ APAttachment();
+ APAttachment(APAttachmentFields fields);
+ ~APAttachment() {};
+ QString get_html_render(HtmlRenderDetails render_info);
+
+private:
+ QString blurhash;
+ std::array<float, 2> focal_point = {0.,0.};
+ QString type; // MIME type, maps to "media_type"
+ QString description; // alt text, maps to "name"
+ QString path_url; // attachment URL, might be file on filesystem (make sure that the attachment dir has been found and that the path is correct)
+ QString filename; // nicer descriptor of the attachment's path
+};
+
+class APAttachmentList : public std::vector<APAttachment>, APBase {
+public:
+ QString get_html_render(HtmlRenderDetails render_info);
+};
diff --git a/src/activitypub/apbase.cpp b/src/activitypub/apbase.cpp
new file mode 100644
index 0000000..52c995d
--- /dev/null
+++ b/src/activitypub/apbase.cpp
@@ -0,0 +1,21 @@
+#include "apbase.h"
+#include <QHash>
+#include <QFile>
+#include <QDebug>
+
+const QString APBase::get_html_template(const QString& template_name) {
+ static QHash<QString, QString>* templates = new QHash<QString, QString>;
+
+ if (not templates->contains(template_name)) {
+ QFile file("src/templates/"+ template_name +".html");
+ if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
+ templates->insert(template_name, "<i>Error loading status_info template.</i>");
+ return templates->value(template_name);
+ }
+
+ templates->insert(template_name, file.readAll());
+ qDebug() << "Loaded HTML template:" << template_name << "Total loaded:" << templates->size(); // print size to make sure that everything is loaded by the same function
+ }
+
+ return templates->value(template_name);
+}
diff --git a/src/activitypub/apbase.h b/src/activitypub/apbase.h
new file mode 100644
index 0000000..3973886
--- /dev/null
+++ b/src/activitypub/apbase.h
@@ -0,0 +1,22 @@
+#pragma once
+
+#include <QString>
+#include <QLocale>
+
+struct HtmlRenderDetails {
+ int text_zone_width;
+ QLocale* locale;
+};
+
+class APBase {
+public:
+ // maybe pass HtmlRenderDetails by reference (pointer) instead of by value? will have to be measured
+ virtual QString get_html_render(HtmlRenderDetails render_info) = 0;
+
+ // having a virtual destructor never hurts. see https://isocpp.org/wiki/faq/virtual-functions#virtual-dtors
+ // it could always be removed by deleting the line below and removing the trivial destructors that are explicitly defined from classes that inherit from APBase.
+ virtual ~APBase() {};
+protected:
+ QString object_url; // this URL represents generally the source of the object. for APObject & derived: object's URL, such as the Note/Statuses' URL for an APPost
+ static const QString get_html_template(const QString& template_name);
+};
diff --git a/src/activitypub/apobject.cpp b/src/activitypub/apobject.cpp
new file mode 100644
index 0000000..28d01b4
--- /dev/null
+++ b/src/activitypub/apobject.cpp
@@ -0,0 +1,23 @@
+#include "apobject.h"
+#include "apbase.h"
+
+APObject::APObject() {}
+
+APObject::APObject(const QString& url) {
+ load_object_url(url);
+}
+
+void APObject::load_object_url(const QString& url) {
+ object_url = url;
+ // TODO: retrieve URL to build APObject.
+ // is it possible to change an object/class from one class type to another? (yeah, i can find a way with a member pointer of type APObject* that points to the right child class and have get_html_render call its get_html_render method when it's time to render)
+ // retriveving would have to be done async and disk/memory cache will have to be checked
+}
+
+const QString APObject::get_object_url() {
+ return object_url;
+}
+
+QString APObject::get_html_render(HtmlRenderDetails render_info) {
+ return QString("<i>Note:</i> object (%1) isn't ready to be rendered as it hasn't been retrieved and built.").arg(object_url);
+}
diff --git a/src/activitypub/apobject.h b/src/activitypub/apobject.h
new file mode 100644
index 0000000..62e60c8
--- /dev/null
+++ b/src/activitypub/apobject.h
@@ -0,0 +1,36 @@
+#pragma once
+
+#include "apbase.h"
+#include "apactor.h"
+#include "apattachment.h"
+#include "../types.h"
+#include <QDateTime>
+
+class APObject : protected APBase {
+public:
+ // An empty object
+ APObject();
+
+ // An object that will be fetched through the remote instance
+ APObject(const QString& url);
+ ~APObject() {};
+
+ // The object will be built from the fetched URL
+ void load_object_url(const QString& url);
+
+ const QString get_object_url();
+
+ virtual QString get_html_render(HtmlRenderDetails render_info);
+
+protected:
+ APActorList to_actors;
+ APActorList cc_actors;
+ APActor by_actor; // maps to "attributedTo"
+ StatusType visibility; // status_type
+
+ QString web_url; // URL that has something like a user accessible web user front-end
+ QString reply_to_url; // in reply to, this is not really a URL but a APPost that could be retrived. maybe have a method for creating that object?
+ QDateTime published; // publishing date
+
+ APAttachmentList attachments;
+};
diff --git a/src/activitypub/appost.cpp b/src/activitypub/appost.cpp
new file mode 100644
index 0000000..dadefe8
--- /dev/null
+++ b/src/activitypub/appost.cpp
@@ -0,0 +1,71 @@
+#include "appost.h"
+
+APPost::APPost(APObjectFields fields) {
+ for (QString actor_url : fields.to_actors)
+ to_actors.push_back(APActor(actor_url));
+ for (QString actor_url : fields.cc_actors)
+ cc_actors.push_back(APActor(actor_url));
+ by_actor = fields.by_actor;
+
+ object_url = fields.object_url;
+ web_url = fields.web_url;
+ reply_to_url = fields.reply_to_url;
+ published = QDateTime::fromString(fields.published, Qt::ISODate);
+
+ for (APAttachmentFields attachment : fields.attachments)
+ attachments.push_back(APAttachment(attachment));
+
+ languages = fields.languages;
+ content = fields.content;
+
+ if (not fields.summary.isEmpty()) {
+ is_sensitive = true;
+ summary = fields.summary;
+ }
+
+ visibility = fields.visibility;
+}
+
+QString APPost::get_html_status_languages() {
+ QString text;
+
+ for (auto lang : languages)
+ text.append(lang + ", ");
+
+ if (text.isEmpty())
+ text = "<em>(unknown)</em>";
+ else
+ text.chop(2); // remove leftover ", "
+
+ return text;
+}
+
+QString APPost::get_html_render(HtmlRenderDetails render_info) {
+ QString html(get_html_template("appost"));
+
+ if (published.isValid()) {
+ // TODO: add a UI setting for configuring the display of local time or UTC time.
+ // Using QLocale::toString() is forward compatible with Qt 6 as QDateTime::toString() will not return anymore a string in the system locale.
+ html.replace("{{published}}", render_info.locale->toString(published.toLocalTime()));
+ }
+
+ html.replace("{{url-id}}", object_url);
+
+ html.replace("{{to}}", to_actors.get_html_render(render_info));
+ html.replace("{{cc}}", cc_actors.get_html_render(render_info));
+ html.replace("{{by}}", by_actor.get_html_render(render_info));
+
+ QString summary_text = summary;
+ if (summary_text.isEmpty())
+ summary_text = "<em>(empty)</em>";
+ html.replace("{{summary}}", summary_text);
+
+ html.replace("{{attachment-div}}", attachments.get_html_render(render_info));
+
+ html.replace("{{url-status}}", web_url);
+
+ html.replace("{{content}}", content);
+ html.replace("{{lang}}", get_html_status_languages());
+
+ return html;
+}
diff --git a/src/activitypub/appost.h b/src/activitypub/appost.h
new file mode 100644
index 0000000..a8b99c5
--- /dev/null
+++ b/src/activitypub/appost.h
@@ -0,0 +1,43 @@
+#pragma once
+
+#include "apobject.h"
+#include "src/activitypub/apbase.h"
+#include "src/types.h"
+#include <vector>
+
+// All these fields (including APAttachmentFields, etc. in other files) are made to map to the fields present in the ActivityStream JSON's "object" for very easy construction.
+// There are additional fields that we don't use so they are not included in the objects (including parent and APActivity).
+
+struct APObjectFields {
+ QStringList to_actors; // Start APObject
+ QStringList cc_actors;
+ QString by_actor;
+ QString object_url;
+ QString web_url;
+ QString reply_to_url;
+ QString published;
+ std::vector<APAttachmentFields> attachments; // End APObject
+ QStringList languages; // This is taken from "content_map"
+ QString content;
+ QString summary;
+ StatusType visibility; // status type discovery is better left to the archive/API parser as this can very between fedi implementations and archive formats
+};
+
+// APPost represents an ActivityPub Note Object
+class APPost : public APObject {
+public:
+ APPost();
+ // A post that will be built from strings, including attachments
+ APPost(APObjectFields fields);
+ ~APPost() {};
+
+ QString get_html_render(HtmlRenderDetails render_info);
+
+protected:
+ bool is_sensitive = false;
+ QString summary; // content warning
+ QStringList languages;
+ QString content;
+
+ QString get_html_status_languages();
+};
diff --git a/src/activitypub/apreblog.cpp b/src/activitypub/apreblog.cpp
new file mode 100644
index 0000000..a6c6c3b
--- /dev/null
+++ b/src/activitypub/apreblog.cpp
@@ -0,0 +1,13 @@
+#include "apreblog.h"
+
+APReblog::APReblog(APReblogFields fields) {
+ object.load_object_url(fields.object); // might have to be fixed
+ visibility = fields.visibility;
+}
+
+QString APReblog::get_html_render(HtmlRenderDetails info) {
+ QString text = "Reblog of this object: <a href=\"{{url}}\">{{url}}</a><hr/>";
+ text.replace("{{url}}", object.get_object_url());
+ text.append(object.get_html_render(info));
+ return text;
+}
diff --git a/src/activitypub/apreblog.h b/src/activitypub/apreblog.h
new file mode 100644
index 0000000..cc340b8
--- /dev/null
+++ b/src/activitypub/apreblog.h
@@ -0,0 +1,27 @@
+#pragma once
+
+#include "apobject.h"
+#include "apobject.h"
+#include "src/types.h"
+#include <vector>
+
+struct APReblogFields {
+ QString object; // the object's URL that was reblogged
+ StatusType visibility; // you can have non-public visibility of reblogs on Mastodon
+};
+
+// APReblog represents an ActivityPub Announce activity, which is a kind of object in a sense. This class is really used just for light bookeeping.
+// The visibility is just the visibility (to & cc) of the APActivity it came from.
+class APReblog : public APObject {
+public:
+ APReblog();
+ // A post that will be built from strings
+ APReblog(APReblogFields fields);
+ ~APReblog() {};
+
+ QString get_html_render(HtmlRenderDetails render_info);
+
+protected:
+ APObject object; // the object that was reblogged (impossible to be an APReblog)
+ StatusType visibility;
+};
diff --git a/src/archive_parser.cpp b/src/archive_parser.cpp
index 63acf60..f8b9179 100644
--- a/src/archive_parser.cpp
+++ b/src/archive_parser.cpp
@@ -1,6 +1,11 @@
#include "archive_parser.h"
#include "src/list_item.h"
#include "src/types.h"
+#include "activitypub/apactivity.h"
+#include "activitypub/apattachment.h"
+#include "activitypub/apobject.h"
+#include "activitypub/apreblog.h"
+#include "activitypub/appost.h"
#include <QFile>
#include <QJsonParseError>
@@ -9,6 +14,7 @@
#include <QDateTime>
#include <QDirIterator>
#include <QHash>
+#include <QDebug>
Archive::Archive(QString outbox_filename, ArchiveType archive_type) :
outbox_filename(outbox_filename), archive_type(archive_type) {}
@@ -64,6 +70,7 @@ bool is_status_type_allowed(StatusType status_type, ViewStatusTypes allowed_type
}
}
+// specific to Mastodon ActivityStreams archives
StatusType get_status_type(QJsonObject obj) {
/*
* public:
@@ -117,6 +124,7 @@ StatusType get_status_type(QJsonObject obj) {
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) {
@@ -131,6 +139,7 @@ void Archive::update_status_list(ViewStatusTypes allowed_types, QListWidget *par
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);
@@ -164,159 +173,136 @@ void Archive::update_status_list(ViewStatusTypes allowed_types, QListWidget *par
}
}
-QString get_html_template(const QString& template_name) {
- static QHash<QString, QString>* templates = new QHash<QString, QString>;
-
- if (not templates->contains(template_name)) {
- QFile file("src/templates/"+ template_name +".html");
- if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
- templates->insert(template_name, "<i>Error loading status_info template.</i>");
- return templates->value(template_name);
- }
-
- qDebug() << "Loaded HTML template:" << template_name;
- templates->insert(template_name, file.readAll());
- }
-
- return templates->value(template_name);
-}
-
-QString get_html_status_object_as_list(QJsonObject obj, QString element_name) {
- QString text;
+QStringList get_status_object_list(QJsonObject obj, QString element_name) {
+ QStringList list;
if (obj.contains(element_name)) {
- QJsonArray to = obj.value(element_name).toArray();
+ QJsonArray elem = obj.value(element_name).toArray();
- for (auto collection : to)
- text.append(collection.toString() + ", ");
+ for (auto collection : elem)
+ list.append(collection.toString());
}
- if (text.isEmpty())
- text = "<em>(empty)</em>";
- else
- text.chop(2); // remove leftover ", "
-
- return text;
+ return list;
}
-QString get_html_status_object_languages(QJsonObject obj, QString element_name = "contentMap") {
- QString text;
+QStringList 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)
- text.append(lang + ", ");
+ list.append(lang);
}
- if (text.isEmpty())
- text = "<em>(unknown)</em>";
- else
- text.chop(2); // remove leftover ", "
-
- return text;
+ return list;
}
-QString Archive::get_html_status_attachments(QJsonValueRef attachments_ref, int text_zone_width) {
- QString text;
+std::vector<APAttachmentFields> Archive::get_status_attachments_list(QJsonValueRef attachments_ref) {
+ std::vector<APAttachmentFields> list;
QJsonArray attachments = attachments_ref.toArray();
- int i = 1;
for (auto attachment_ref : attachments) {
- QString att_html(get_html_template("attachment"));
- QJsonObject attachment = attachment_ref.toObject();
+ APAttachmentFields element;
- att_html.replace("{{id}}", QString::number(i));
+ 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() + "/");
- att_html.replace("{{path}}", url);
- att_html.replace("{{filename}}", QFileInfo(url).fileName()); // FIXME: this is ugly
- }
- if (attachment.contains("mediaType") and attachment["mediaType"].toString().startsWith("image/"))
- // dynamically resize image based on the display widget size to avoid horizontal scrolling
- att_html.replace("{{img-width}}", QString::number((float)text_zone_width - (float)text_zone_width*0.15));
- else
- att_html.replace("{{img-width}}", "0");
+ element.path = url;
+ element.filename = QFileInfo(url).fileName();
+ }
- if (attachment.contains("name"))
- att_html.replace("{{alt-text}}", attachment["name"].toString());
+ element.media_type = attachment["mediaType"].toString();
+ element.name = attachment["name"].toString();
- text.append(att_html);
- ++i;
+ list.push_back(element);
}
- return text;
+ return list;
}
// status_index is assumed to be a valid index
QString Archive::get_html_status_info(int status_index, int text_zone_width, StatusType status_type, QLocale* locale) {
- QString info_text(get_html_template("status_info"));
+ // the JSON AP Activity (TODO: fix misleading name)
QJsonObject obj = outbox_items->at(status_index).toObject();
- if (obj.contains("type") and obj["type"].isString())
- info_text.replace("{{type}}", obj["type"].toString());
+ APActivityFields act_fields = {
+ .visibility = status_type
+ };
+
+ QString obj_url;
- if (obj.contains("published") and obj["published"].isString()) {
- // TODO: add a UI setting for configuring the display of local time or UTC time.
- QDateTime date = QDateTime::fromString(obj["published"].toString(), Qt::ISODate).toLocalTime();
- // Using QLocale::toString() is forward compatible with Qt 6 as QDateTime::toString() will not return anymore a string in the system locale.
- info_text.replace("{{published}}", locale->toString(date));
+ // The post
+ APObjectFields obj_fields;
+
+ if (obj.contains("type") and obj["type"].isString()) {
+ QString type = obj["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 (obj.contains("published") and obj["published"].isString())
+ act_fields.published = obj["published"].toString();
+
if (obj.contains("id") and obj["id"].isString())
- info_text.replace("{{url-id}}", obj["id"].toString());
+ act_fields.object_url = obj["id"].toString();
- info_text.replace("{{to}}", get_html_status_object_as_list(obj, "to"));
- info_text.replace("{{cc}}", get_html_status_object_as_list(obj, "cc"));
+ if (obj.contains("actor"))
+ act_fields.by_actor = obj["actor"].toString(); // returns Null string if actor is not a string
+
+ act_fields.to_actors = get_status_object_list(obj, "to");
+ act_fields.cc_actors = get_status_object_list(obj, "cc");
if (obj["object"].isObject()) {
+ // the JSON AP Object (TODO: fix misleading name)
QJsonObject activity = obj["object"].toObject();
- if (activity.contains("summary")) {
- QString summary_text = activity["summary"].toString();
- if (summary_text.isEmpty())
- summary_text = "<em>(empty)</em>";
- info_text.replace("{{summary}}", summary_text);
- }
+ obj_fields.visibility = get_status_type(activity);
- info_text.replace("{{attachment-div}}", get_html_status_attachments(activity["attachment"], text_zone_width));
+ if (activity.contains("summary"))
+ obj_fields.summary = activity["summary"].toString();
- if (activity.contains("url") and activity["url"].isString())
- info_text.replace("{{url-status}}", activity["url"].toString());
+ obj_fields.attachments = get_status_attachments_list(activity["attachment"]);
+
+ obj_fields.web_url = activity["url"].toString();
+ obj_fields.object_url = activity["atomUri"].toString(); // atomUri?
+ obj_fields.reply_to_url = activity["inReplyTo"].toString();
if (activity["content"].isString()) {
- info_text.replace("{{content}}", activity["content"].toString());
- info_text.replace("{{lang}}", get_html_status_object_languages(activity));
+ obj_fields.content = activity["content"].toString();
+ obj_fields.languages = get_status_object_language_list(activity);
}
- } else if (obj["object"].isString()) {
- QString text = obj["object"].toString();
- QString content_text;
-
- if (status_type == REBLOG) {
- content_text = "Reblog of this object: <a href=\"{{url}}\">{{url}}</a>";
- content_text.replace("{{url}}", text);
- info_text.replace("{{url-status}}", text);
- } else
- content_text = text;
-
- info_text.replace("{{content}}", content_text);
- info_text.replace("{{summary}}", "<em>(empty)</em>");
- info_text.replace("{{lang}}", "<em>(not applicable)</em>");
- info_text.replace("{{attachment-div}}", "");
- }
- return info_text;
+ obj_fields.to_actors = get_status_object_list(activity, "to");
+ obj_fields.cc_actors = get_status_object_list(activity, "cc");
+ obj_fields.by_actor = activity["attributedTo"].toString();
+ obj_fields.published = activity["published"].toString();
+
+ } else if (obj["object"].isString())
+ obj_url = obj["object"].toString();
+
+ if (obj_url.isNull())
+ act_fields.object = new APPost(obj_fields);
+ else
+ act_fields.object = new APReblog({obj_url, act_fields.visibility});
+
+ // 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.
QString Archive::get_html_status_text(int status_index) {
QString text("");
QJsonObject obj = outbox_items->at(status_index).toObject();
diff --git a/src/archive_parser.h b/src/archive_parser.h
index aa3bc8a..d9313ee 100644
--- a/src/archive_parser.h
+++ b/src/archive_parser.h
@@ -8,6 +8,7 @@
#include <QDir>
#include "types.h"
+#include "activitypub/apattachment.h"
enum ArchiveType {
MASTODON
@@ -42,6 +43,6 @@ private:
QJsonObject *outbox_json = nullptr;
QJsonArray *outbox_items = nullptr;
- QString get_html_status_attachments(QJsonValueRef attachments_ref, int text_zone_width);
+ std::vector<APAttachmentFields> get_status_attachments_list(QJsonValueRef attachments_ref);
void find_attachment_dir(QString example_attachment);
};
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index b9c4e8e..ede24f8 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -63,6 +63,7 @@ void MainWindow::on_actionAbout_triggered(bool checked) {
void MainWindow::on_listWidget_itemActivated(QListWidgetItem *item) {
StatusListItem* status = dynamic_cast<StatusListItem*>(item);
if (status != nullptr) {
+ // TODO: do this in another thread
QString status_info = status->get_info_html(ui->statusInfoText->width(), &locale_context);
ui->statusInfoText->setHtml(status_info);
}
diff --git a/src/templates/apactivity.html b/src/templates/apactivity.html
new file mode 100644
index 0000000..298c28c
--- /dev/null
+++ b/src/templates/apactivity.html
@@ -0,0 +1,16 @@
+<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>Status info</title>
+ </head>
+ <body>
+ <h1 id="type">{{type}}</h1>
+ <p>Activity info:</p>
+ <ul>
+ <li id="act-publication-date"><b>Published:</b> {{published}}</li>
+ <li id="act-by"><b>By:</b> {{by}}</li>
+ </ul>
+ {{object}}
+ </body>
+</html>
diff --git a/src/templates/apattachment.html b/src/templates/apattachment.html
new file mode 100644
index 0000000..3befd3c
--- /dev/null
+++ b/src/templates/apattachment.html
@@ -0,0 +1,3 @@
+<img src="{{path}}" style="float:none;" width="{{img-width}}"/>
+<p><a href="{{path}}">{{filename}}</a></p>
+<p style="text-indent:5px;"><b>Alt:</b> {{alt-text}}</p>
diff --git a/src/templates/apattachmentlist_item.html b/src/templates/apattachmentlist_item.html
new file mode 100644
index 0000000..dc2fd67
--- /dev/null
+++ b/src/templates/apattachmentlist_item.html
@@ -0,0 +1,3 @@
+<h2>Attachment {{id}}</h2><br/>
+<div>{{attachment}}</div>
+<hr width="80%" />
diff --git a/src/templates/appost.html b/src/templates/appost.html
new file mode 100644
index 0000000..12d2f1e
--- /dev/null
+++ b/src/templates/appost.html
@@ -0,0 +1,13 @@
+<p>Post info:</p>
+<ul>
+ <li id="publication-date"><b>Published:</b> {{published}}</li>
+ <li id="to"><b>To:</b> {{to}}</li>
+ <li id="cc"><b>CC:</b> {{cc}}</li>
+ <li id="by"><b>By:</b> {{by}}</li>
+ <li id="lang"><b>Language:</b> {{lang}}</li>
+ <li>URL to <a href="{{url-status}}">Status</a>, <a href="{{url-id}}">JSON-LD Object</a></li>
+</ul>
+
+<div><tt id="content-warning"><b>CW:</b> {{summary}}</tt></div>
+<div id="content">{{content}}</div>
+<div id="attachments">{{attachment-div}}</div>
diff --git a/src/templates/apreblog.html b/src/templates/apreblog.html
new file mode 100644
index 0000000..da99268
--- /dev/null
+++ b/src/templates/apreblog.html
@@ -0,0 +1,4 @@
+<ul>
+<li>URL to <a href="{{url-status}}">Status</a></li>
+</ul>
+<div id="content">{{content}}</div>