aboutsummaryrefslogtreecommitdiffstats
path: root/src/archive_parser.cpp
blob: 2522ffae8c4f3999365bce433f9ac26cf8aff411 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
#include "archive_parser.h"
#include "src/list_item.h"
#include "src/types.h"

#include <QFile>
#include <QJsonParseError>
#include <QTextDocument>
#include <QFileInfo>
#include <QDateTime>

Archive::Archive(QString outbox_filename, ArchiveType archive_type) :
    outbox_filename(outbox_filename), archive_type(archive_type) {}

Archive::InitError 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 (outbox_json->contains("@context") and outbox_json->value("@context").isString() and
        (outbox_json->value("@context").toString() == "https://www.w3.org/ns/activitystreams")))
        return JsonNotActivityStream;
    if (outbox_json->contains("orderedItems") and outbox_json->value("orderedItems").isArray()) {
        outbox_items = new QJsonArray(outbox_json->value("orderedItems").toArray()); // we'll need it during Archive's lifetime
    }

    return NoError;
}

Archive::~Archive() {
    delete outbox_json; outbox_json = nullptr;
    delete outbox_items; outbox_items = nullptr;
}

bool 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;
    }
}

StatusType 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;
}

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
            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());

                    ListItem *item = new ListItem(strip_html.toPlainText(), status_type, has_attachment, parent, i);
                }
            } else if (activity_type == "Announce" and obj["object"].isString()) {
                ListItem *item = new ListItem(activity_type, REBLOG, false, parent, i);
            }
        }
        next_item:
        ++i;
    }
}

QString* get_html_status_info_template() {
    static QString* html = new QString();

    if (html->isNull()) {
        QFile file("src/templates/status_info.html");
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            html->append("<i>Error loading status_info template.</i>");
            return html;
        }

        html->append(file.readAll());
    }

    return html;
}

QString* get_html_status_info_attachment_template() {
    static QString* html = new QString();

    if (html->isNull()) {
        QFile file("src/templates/attachment.html");
        if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
            html->append("<i>Error loading attachment template.</i>");
            return html;
        }

        html->append(file.readAll());
    }

    return html;
}

QString get_html_status_object_as_list(QJsonObject obj, QString element_name) {
    QString text;

    if (obj.contains(element_name)) {
        QJsonArray to = obj.value(element_name).toArray();

        for (auto collection : to)
            text.append(collection.toString() + ", ");
    }

    if (text.isEmpty())
        text = "<em>(empty)</em>";
    else
        text.chop(2); // remove leftover ", "

    return text;
}

QString get_html_status_object_languages(QJsonObject obj, QString element_name = "contentMap") {
    QString text;

    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 + ", ");
    }

    if (text.isEmpty())
        text = "<em>(unknown)</em>";
    else
        text.chop(2); // remove leftover ", "

    return text;
}

QString Archive::get_html_status_attachments(QJsonValueRef attachments_ref, int text_zone_width) {
    QString text;
    QJsonArray attachments = attachments_ref.toArray();

    int i = 1;
    for (auto attachment_ref : attachments) {
        QString att_html(*get_html_status_info_attachment_template());
        QJsonObject attachment = attachment_ref.toObject();

        att_html.replace("{{id}}", QString::number(i));

        if (attachment.contains("url")) {
            QString url = attachment["url"].toString();
            QFileInfo path(outbox_filename);
            url.prepend(path.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");

        if (attachment.contains("name"))
            att_html.replace("{{alt-text}}", attachment["name"].toString());

        text.append(att_html);
        ++i;
    }

    return text;
}

// 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_status_info_template());
    QJsonObject obj = outbox_items->at(status_index).toObject();

    if (obj.contains("type") and obj["type"].isString())
        info_text.replace("{{type}}", obj["type"].toString());

    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));
    }

    if (obj.contains("id") and obj["id"].isString())
        info_text.replace("{{url-id}}", 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["object"].isObject()) {
        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);
        }

        info_text.replace("{{attachment-div}}", get_html_status_attachments(activity["attachment"], text_zone_width));

        if (activity.contains("url") and activity["url"].isString())
            info_text.replace("{{url-status}}", activity["url"].toString());

        if (activity["content"].isString()) {
            info_text.replace("{{content}}", activity["content"].toString());
            info_text.replace("{{lang}}", get_html_status_object_languages(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;
}

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("<p><b>CW:</b> {{summary}}</p>").replace("{{summary}}", summary_text));
        }

        if (activity["content"].isString()) {
            text.append(QString("<div>{{content}}</div>").replace("{{content}}", activity["content"].toString()));
        }
    }

    return text;
}