aboutsummaryrefslogtreecommitdiffstats
path: root/src/archive_parser.cpp
blob: d9d4cf19d4a02e5c52fe34df8e3d82ae65c170bb (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
#include "archive_parser.h"
#include "src/list_item.h"
#include "src/types.h"

#include <QFile>
#include <QJsonParseError>
#include <QTextDocument>

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.
    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;
        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 yourself 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;
            if (obj.value("type").toString() != "Create") // ignoring everything that isn't a Create activity for now
                goto next_item;

            // Determine the status' type
            StatusType status_type = get_status_type(obj);
            if (not is_status_type_allowed(status_type, allowed_types))
                goto next_item;

            if (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);
                }
            }
        }
        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_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;
}

// status_index is assumed to be a valid index
QString Archive::get_html_status_info(int status_index) {
    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())
        info_text.replace("{{published}}", obj["published"].toString());

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

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

    return info_text;
}