From 9c60bc8ce4a0990c228d2cd746791b88c41532b8 Mon Sep 17 00:00:00 2001 From: ConfuSomu Date: Thu, 24 Aug 2023 18:43:15 -0400 Subject: Implement basic Mastodon API support Implement support for OAuth 2.0 code entry, which is then used to retrieve a token that is stored in the application's settings. Authentification allowed the implementation of a basic "get post from URL" feature mostly made for testing. --- src/net/instance.cpp | 24 ++++++++ src/net/instance.h | 24 ++++++++ src/net/mastodon_instance.cpp | 134 ++++++++++++++++++++++++++++++++++++++++++ src/net/mastodon_instance.h | 19 ++++++ 4 files changed, 201 insertions(+) create mode 100644 src/net/instance.cpp create mode 100644 src/net/instance.h create mode 100644 src/net/mastodon_instance.cpp create mode 100644 src/net/mastodon_instance.h (limited to 'src/net') 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("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 +#include +#include +#include +#include + +APPost* post_from_json(const QJsonObject &status); + +MastodonInstance::MastodonInstance() : instance(SettingsInterface::quick_read_setting("net/instance/address").toStdString(), SettingsInterface::quick_read_setting("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; +}; -- cgit v1.2.3-54-g00ecf