aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--CMakeLists.txt7
-rw-r--r--src/mainwindow.cpp19
-rw-r--r--src/mainwindow.h1
-rw-r--r--src/mainwindow.ui6
-rw-r--r--src/net/instance.cpp24
-rw-r--r--src/net/instance.h24
-rw-r--r--src/net/mastodon_instance.cpp134
-rw-r--r--src/net/mastodon_instance.h19
-rw-r--r--src/settingsdialog.cpp27
9 files changed, 260 insertions, 1 deletions
diff --git a/CMakeLists.txt b/CMakeLists.txt
index f5e6f18..15486f5 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -15,6 +15,8 @@ set(CMAKE_EXPORT_COMPILE_COMMANDS ON) # For Kate's LSP
find_package(QT NAMES Qt5 REQUIRED COMPONENTS Widgets Concurrent)
find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets Concurrent)
+find_package(mastodonpp REQUIRED)
+
# add_subdirectory will happen later…
set(PROJECT_SOURCES
src/main.cpp
@@ -38,6 +40,10 @@ set(PROJECT_SOURCES
src/list_item.h
src/command_line.cpp
src/command_line.h
+ src/net/instance.cpp
+ src/net/instance.h
+ src/net/mastodon_instance.cpp
+ src/net/mastodon_instance.h
src/activitypub/fields.h
src/activitypub/apactivity.h
src/activitypub/apactor.h
@@ -81,6 +87,7 @@ else()
endif()
target_link_libraries(ActorViewer PRIVATE Qt${QT_VERSION_MAJOR}::Widgets)
+target_link_libraries(ActorViewer PUBLIC mastodonpp)
set_target_properties(ActorViewer PROPERTIES
MACOSX_BUNDLE_GUI_IDENTIFIER space.twilightsparkle.ActorViewer
diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp
index c6203d4..4ab0bb2 100644
--- a/src/mainwindow.cpp
+++ b/src/mainwindow.cpp
@@ -1,12 +1,15 @@
#include "mainwindow.h"
#include "./ui_mainwindow.h"
+#include "src/activitypub/appost.h"
#include "src/archive/base_archive.h"
#include "src/finddialog.h"
#include "src/list_item.h"
#include "src/command_line.h"
#include "src/settingsdialog.h"
+#include "src/net/instance.h"
#include <QFileDialog>
+#include <QInputDialog>
#include <QMessageBox>
#include <QRandomGenerator>
#include <QClipboard>
@@ -148,6 +151,22 @@ void MainWindow::set_search_text(const QString &text) {
ui->textInputSearch->setText(text);
}
+void MainWindow::on_actionOpen_URL_triggered(bool checked) {
+ bool ok;
+ QString url = QInputDialog::getText(this, tr("Open status from URL"), tr("Status URL:"), QLineEdit::Normal, "https://…", &ok);
+
+ // TODO: Move all of this to another thread
+ // TODO: Reuse the Instance object
+ // Really hacky code but works as a PoC and allows testing
+ if (ok and not url.isEmpty()) {
+ Instance* instance = Instance::create_instance();
+ APPost* post = instance->get_post_from_url(url);
+ QString html = post->get_html_render({ui->statusInfoText->width(), &locale_context});
+ ui->statusInfoText->setHtml(html);
+ delete instance; instance = nullptr;
+ } else return;
+}
+
void MainWindow::relist_statuses() {
if (data_archive) {
ui->listWidget->clear();
diff --git a/src/mainwindow.h b/src/mainwindow.h
index 4c7e034..24bc970 100644
--- a/src/mainwindow.h
+++ b/src/mainwindow.h
@@ -49,6 +49,7 @@ private slots:
void on_actionRandom_status_triggered(bool checked);
void on_actionCopy_status_triggered(bool checked);
void on_actionFind_triggered(bool checked);
+ void on_actionOpen_URL_triggered(bool checked);
void on_textInputSearch_textEdited(const QString &text);
diff --git a/src/mainwindow.ui b/src/mainwindow.ui
index 292930d..2d7cf30 100644
--- a/src/mainwindow.ui
+++ b/src/mainwindow.ui
@@ -123,6 +123,7 @@ p, li { white-space: pre-wrap; }
<string>Go</string>
</property>
<addaction name="actionRandom_status"/>
+ <addaction name="actionOpen_URL"/>
</widget>
<widget class="QMenu" name="menuEdit">
<property name="title">
@@ -271,6 +272,11 @@ p, li { white-space: pre-wrap; }
<enum>QAction::PreferencesRole</enum>
</property>
</action>
+ <action name="actionOpen_URL">
+ <property name="text">
+ <string>Open URL…</string>
+ </property>
+ </action>
</widget>
<tabstops>
<tabstop>listWidget</tabstop>
diff --git a/src/net/instance.cpp b/src/net/instance.cpp
new file mode 100644
index 0000000..c3af86c
--- /dev/null
+++ b/src/net/instance.cpp
@@ -0,0 +1,24 @@
+#include "instance.h"
+#include "mastodon_instance.h"
+#include "src/settings_interface.h"
+
+Instance* Instance::create_instance() {
+ return create_instance(SettingsInterface::quick_read_setting<AppSettingsTypes::InstanceType>("net/instance/type"));
+}
+
+Instance* Instance::create_instance(AppSettingsTypes::InstanceType type) {
+ switch (type) {
+ case AppSettingsTypes::InstanceType::MASTODON:
+ return new MastodonInstance;
+ default:
+ return nullptr;
+ }
+}
+
+QString Instance::oauth2_step1() {
+ return "";
+}
+
+Instance::OAuth2Step2 Instance::oauth2_step2(const QString &auth_code) {
+ return {false};
+}
diff --git a/src/net/instance.h b/src/net/instance.h
new file mode 100644
index 0000000..2b7eec7
--- /dev/null
+++ b/src/net/instance.h
@@ -0,0 +1,24 @@
+#pragma once
+#include "src/activitypub/appost.h"
+#include "src/settings_interface.h"
+
+class Instance {
+public:
+ struct OAuth2Step2 {
+ bool ok;
+ QString token;
+ };
+
+ virtual ~Instance() {};
+ virtual APPost* get_post_from_url(const QString &url) = 0;
+ // Returns the URI the user has to visit in their browser
+ virtual QString oauth2_step1();
+ // auth_code is the code given by the user through the URI
+ // Returns true if authenfication was successful.
+ virtual OAuth2Step2 oauth2_step2(const QString &auth_code);
+ bool supports_oauth2 = false;
+ // Creates an instance by reading the type from settings
+ static Instance* create_instance();
+ // Creates a specific type of instance, for use with SettingsDialog
+ static Instance* create_instance(AppSettingsTypes::InstanceType type);
+};
diff --git a/src/net/mastodon_instance.cpp b/src/net/mastodon_instance.cpp
new file mode 100644
index 0000000..8b88221
--- /dev/null
+++ b/src/net/mastodon_instance.cpp
@@ -0,0 +1,134 @@
+#include "mastodon_instance.h"
+#include "src/activitypub/appost.h"
+#include "src/activitypub/apquestion.h"
+#include "src/settings_interface.h"
+#include "src/types.h"
+
+#include <QJsonDocument>
+#include <QJsonObject>
+#include <QJsonArray>
+#include <QUrl>
+#include <QDebug>
+
+APPost* post_from_json(const QJsonObject &status);
+
+MastodonInstance::MastodonInstance() : instance(SettingsInterface::quick_read_setting<QString>("net/instance/address").toStdString(), SettingsInterface::quick_read_setting<QString>("net/instance/token").toStdString()), connection(instance) {
+
+}
+
+MastodonInstance::~MastodonInstance() {
+ if (obtain_token)
+ delete obtain_token;
+}
+
+QString MastodonInstance::oauth2_step1() {
+ obtain_token = new mastodonpp::Instance::ObtainToken{instance};
+ auto answer{obtain_token->step_1("ActorViewer", "read", "https://twilightsparkle.space")};
+ if (answer)
+ return answer.body.c_str();
+ else
+ return "";
+}
+
+Instance::OAuth2Step2 MastodonInstance::oauth2_step2(const QString &auth_code) {
+ if (!obtain_token) return {false};
+ auto answer{obtain_token->step_2(auth_code.toStdString())};
+ QString token;
+
+ if (answer) {
+ token = answer.body.c_str();
+ }
+
+ delete obtain_token; obtain_token = nullptr;
+ return {(bool)answer, token};
+}
+
+APPost* MastodonInstance::get_post_from_url(const QString &url) {
+ auto answer{connection.get(mastodonpp::API::v2::search, {
+ {"q", QUrl::toPercentEncoding(url).toStdString()},
+ {"type", "statuses"},
+ {"limit", "1"},
+ {"resolve", "true"}
+ })};
+
+ if (answer) {
+ QJsonDocument doc = QJsonDocument::fromJson(answer.body.c_str());
+ QJsonObject obj = doc.object();
+ qDebug() << doc;
+ if (obj.contains("statuses")) {
+ QJsonObject status = obj["statuses"].toArray().first().toObject();
+ if (status.isEmpty()) return new APPost({.content="invalid"}); // Invalid
+ return post_from_json(status);
+ }
+ }
+
+ return new APPost({.content="connection error"});
+}
+
+// Create a filled APPost object from a Status entity (https://docs.joinmastodon.org/entities/Status/)
+// We have to return a pointer as else I can't get derived classes of APPost to render properly: the additional methods are ignored.
+// Furthermore, using pointers removes unecessary copying, even if that can be reduced by return value optimization.
+APPost* post_from_json(const QJsonObject &status) {
+ APObjectFields fields;
+ bool is_question = status.contains("poll");
+
+ fields.by_actor = status["account"]["url"].toString();
+ fields.reply_to_url = status["in_reply_to_id"].toString(); // id ≠ url
+
+ fields.languages.append(status["language"].toString());
+ fields.published = status["created_at"].toString();
+
+ fields.object_url = status["uri"].toString();
+ fields.web_url = status["url"].toString();
+
+ if (status["sensitive"].toBool(true))
+ fields.summary = status["spoiler_text"].toString();
+ if (status.contains("content"))
+ fields.content = status["content"].toString();
+
+ if (status.contains("visibility")) {
+ QString visibility = status["visibility"].toString();
+ if (visibility == "public") fields.visibility = StatusType::PUBLIC;
+ else if (visibility == "unlisted") fields.visibility = StatusType::UNLISTED;
+ else if (visibility == "private") fields.visibility = StatusType::PRIVATE;
+ else if (visibility == "direct") fields.visibility = StatusType::DIRECT;
+ else fields.visibility = StatusType::UNKNOWN;
+ }
+
+ if (QJsonArray medias = status["media_attachments"].toArray(); not medias.isEmpty()) {
+ for (QJsonValueRef media : medias) {
+ if (not media.isObject()) continue;
+ QJsonObject attrib = media.toObject();
+ fields.attachments.push_back({
+ // FIXME: we use the preview to lower bandwidth but it would be nicer to have APAttachment fields for differencing between qualities.
+ .path = attrib["preview_url"].toString(),
+ // FIXME: we don't have any better attribute to work with
+ .filename = attrib["id"].toString(),
+ .media_type = attrib["type"].toString(),
+ .name = attrib["description"].toString()
+ // TODO: we don't use the blurhash yet…
+ });
+ }
+ }
+
+ if (is_question) {
+ QJsonObject poll = status["poll"].toObject();
+
+ fields.question.end_time = poll["expires_at"].toString();
+ fields.question.closed_time = poll["expires_at"].toString();
+ fields.question.total_votes = poll["votes_count"].toInt();
+
+ for (QJsonValueRef option : poll["options"].toArray()) {
+ QJsonObject element = option.toObject();
+ fields.question.poll_options.push_back({element["title"].toString(), element["votes_count"].toInt()});
+ }
+
+
+ // TODO: have an boolean attribute for defining if the poll is multiple choice or not, int for the voters_count and bool/int for `voted` and `own_votes`
+ }
+
+ if (is_question)
+ return new APQuestion(fields);
+ else
+ return new APPost(fields);
+}
diff --git a/src/net/mastodon_instance.h b/src/net/mastodon_instance.h
new file mode 100644
index 0000000..983bef6
--- /dev/null
+++ b/src/net/mastodon_instance.h
@@ -0,0 +1,19 @@
+#pragma once
+#include "instance.h"
+#include "mastodonpp/instance.hpp"
+#include "mastodonpp/connection.hpp"
+
+class MastodonInstance : public Instance {
+public:
+ MastodonInstance();
+ ~MastodonInstance();
+ APPost* get_post_from_url(const QString &url);
+ QString oauth2_step1();
+ OAuth2Step2 oauth2_step2(const QString &auth_code);
+ bool supports_oauth2 = true;
+
+private:
+ mastodonpp::Instance instance;
+ mastodonpp::Connection connection;
+ mastodonpp::Instance::ObtainToken* obtain_token = nullptr;
+};
diff --git a/src/settingsdialog.cpp b/src/settingsdialog.cpp
index 213859a..051091f 100644
--- a/src/settingsdialog.cpp
+++ b/src/settingsdialog.cpp
@@ -1,6 +1,14 @@
#include "settingsdialog.h"
+#include "src/net/instance.h"
+#include "src/settings_interface.h"
+
#include <QPushButton>
#include <QDialogButtonBox>
+#include <QDesktopServices>
+#include <QUrl>
+#include <QMessageBox>
+#include <QInputDialog>
+#include <qnamespace.h>
SettingsDialog::SettingsDialog(QWidget* parent)
: QDialog(parent, Qt::Dialog), ui(new Ui::SettingsDialog)
@@ -64,7 +72,24 @@ void SettingsDialog::on_instanceActionsLabel_linkActivated(const QString& link)
ui->instanceAddressLineEdit->setText(instance_address);
update_ui_in_progress = false;
} else if (link == "action:request-token") {
- // TODO: open browser with token request URL
+ Instance* instance = Instance::create_instance((AppSettingsTypes::InstanceType)ui->instanceTypeComboBox->currentIndex());
+
+ QString url = instance->oauth2_step1();
+ if (not QDesktopServices::openUrl(url))
+ QMessageBox::information(this, tr("Navigate to this URL"), tr("Please open the following URL in your browser: %1").arg(1));
+
+ Instance::OAuth2Step2 step2 = {.ok = false};
+ while (not step2.ok) {
+ bool ok;
+ QString code = QInputDialog::getText(this, tr("Enter authorization code"), tr("Enter the code given during the authentification flow:"), QLineEdit::Normal, "", &ok);
+
+ if (code.isEmpty()) return; // User canceled
+ if (ok) step2 = instance->oauth2_step2(code);
+ }
+
+ // Do not set update_ui_in_progress as we want the new text to be seen as new text by settings_interface (through on_tokenLineEdit_editingFinished())
+ ui->tokenLineEdit->setText(step2.token);
+ on_tokenLineEdit_editingFinished(); // Make sure that the change has been noticed as sometimes it isn't
}
}