From b40223926238e141444e70ffe8472902565ebd8c Mon Sep 17 00:00:00 2001 From: blankie Date: Fri, 24 Nov 2023 23:46:00 +1100 Subject: [PATCH] Add about page --- CMakeLists.txt | 2 +- client.cpp | 15 ++++++++++++++ client.h | 3 +++ main.cpp | 5 +++++ models.cpp | 14 +++++++++++++ models.h | 10 ++++++++++ routes/about.cpp | 51 ++++++++++++++++++++++++++++++++++++++++++++++++ routes/css.cpp | 17 ++++++++++++++-- routes/routes.h | 1 + 9 files changed, 115 insertions(+), 3 deletions(-) create mode 100644 routes/about.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index b01d81e..d86d122 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -30,7 +30,7 @@ add_link_options(${FLAGS}) add_executable(${PROJECT_NAME} main.cpp numberhelper.cpp config.cpp models.cpp client.cpp servehelper.cpp timeutils.cpp - routes/home.cpp routes/css.cpp routes/user.cpp routes/status.cpp routes/tags.cpp + routes/home.cpp routes/css.cpp routes/user.cpp routes/status.cpp routes/tags.cpp routes/about.cpp blankie/serializer.cpp blankie/escape.cpp) set_target_properties(${PROJECT_NAME} PROPERTIES diff --git a/client.cpp b/client.cpp index f6ca938..22432a7 100644 --- a/client.cpp +++ b/client.cpp @@ -171,6 +171,21 @@ std::vector MastodonClient::get_tag_timeline(const std::string& host, cons return posts; } +Instance MastodonClient::get_instance(const std::string& host) { + using namespace std::string_literals; + + Instance instance = this->_send_request("https://"s + host + "/api/v2/instance"); + instance.contact_account.same_server = instance.contact_account.server == host; + return instance; +} + +blankie::html::HTMLString MastodonClient::get_extended_description(const std::string& host) { + using namespace std::string_literals; + + nlohmann::json j = this->_send_request("https://"s + host + "/api/v1/instance/extended_description"); + return blankie::html::HTMLString(j.at("content").get()); +} + CURL* MastodonClient::_get_easy() { CURL* curl = pthread_getspecific(this->_easy_key); if (!curl) { diff --git a/client.h b/client.h index 0e5a13d..1672a7e 100644 --- a/client.h +++ b/client.h @@ -73,6 +73,9 @@ public: std::vector get_tag_timeline(const std::string& host, const std::string& tag, std::optional max_id); + Instance get_instance(const std::string& host); + blankie::html::HTMLString get_extended_description(const std::string& host); + private: CURL* _get_easy(); nlohmann::json _send_request(const std::string& url); diff --git a/main.cpp b/main.cpp index bbf1176..f8379f5 100644 --- a/main.cpp +++ b/main.cpp @@ -49,6 +49,11 @@ int main(int argc, char** argv) { // protect against .. server.Get("/(" DOMAIN_RE ")/tags/(?!\\.|%2E|%2e)(.+)", tags_route); + server.Get("/(" DOMAIN_RE ")/about", about_route); + server.Get("/(" DOMAIN_RE ")/?", [](const httplib::Request& req, httplib::Response& res) { + serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1) + "/about", false); + }); + server.Get("/https://?(.+)", [](const httplib::Request& req, httplib::Response& res) { serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1), true); }); diff --git a/models.cpp b/models.cpp index fd295f6..7dcb122 100644 --- a/models.cpp +++ b/models.cpp @@ -127,6 +127,20 @@ void from_json(const json& j, PostContext& context) { j.at("descendants").get_to(context.descendants); } +void from_json(const json& j, Instance& instance) { + j.at("title").get_to(instance.title); + j.at("description").get_to(instance.description); + j.at("thumbnail").at("url").get_to(instance.thumbnail); + j.at("contact").at("email").get_to(instance.contact_email); + j.at("contact").at("account").get_to(instance.contact_account); + + json rules = j.at("rules"); + instance.rules.reserve(rules.size()); + for (const json& i : rules) { + instance.rules.push_back(i.at("text").get()); + } +} + static std::regex rfc3339_re(R"EOF((\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(?:\.\d+)?(?:(Z)|([+-]\d{2}):(\d{2})))EOF", std::regex::ECMAScript | std::regex::icase); time_t parse_rfc3339(const std::string& str) { diff --git a/models.h b/models.h index 0fa03ff..ca1f4e0 100644 --- a/models.h +++ b/models.h @@ -96,6 +96,15 @@ struct PostContext { std::vector descendants; }; +struct Instance { + std::string title; + std::string description; + std::string thumbnail; + std::string contact_email; + Account contact_account; + std::vector rules; +}; + void from_json(const nlohmann::json& j, Emoji& emoji); void from_json(const nlohmann::json& j, AccountField& field); void from_json(const nlohmann::json& j, Account& account); @@ -104,3 +113,4 @@ void from_json(const nlohmann::json& j, PollOption& option); void from_json(const nlohmann::json& j, Poll& poll); void from_json(const nlohmann::json& j, Post& post); void from_json(const nlohmann::json& j, PostContext& context); +void from_json(const nlohmann::json& j, Instance& instance); diff --git a/routes/about.cpp b/routes/about.cpp new file mode 100644 index 0000000..354281b --- /dev/null +++ b/routes/about.cpp @@ -0,0 +1,51 @@ +#include "routes.h" +#include "../servehelper.h" +#include "../client.h" +#include "../models.h" + +void about_route(const httplib::Request& req, httplib::Response& res) { + using namespace std::string_literals; + + std::string server = req.matches.str(1); + + Instance instance; + blankie::html::HTMLString extended_description; + try { + instance = mastodon_client.get_instance(server); + extended_description = mastodon_client.get_extended_description(server); + } catch (const std::exception& e) { + res.status = 500; + serve_error(req, res, "500: Internal server error", "Failed to fetch instance information", e.what()); + return; + } + + Element rules("ol"); + rules.nodes.reserve(instance.rules.size()); + for (const std::string& rule : instance.rules) { + rules.nodes.push_back(Element("li", {rule})); + } + + Element body("body", { + Element("a", {{"href", instance.thumbnail}}, { + Element("img", {{"class", "about_page-header"}, {"src", instance.thumbnail}}, {}), + }), + Element("p", { + Element("b", {instance.title, ":"}), " ", instance.description, + Element("br"), Element("b", {"Email:"}), " ", Element("a", {{"href", "mailto:"s + instance.contact_email}}, {instance.contact_email}), + Element("br"), Element("b", {"Administered by:"}), " ", Element("a", {{"href", get_origin(req) + '/' + server + "/@" + instance.contact_account.acct(false)}}, { + preprocess_html(req, instance.contact_account.emojis, instance.contact_account.display_name), " (@", instance.contact_account.acct(false), ")", + }), + }), + + Element("details", {{"class", "about_page-about"}, {"open", ""}}, { + Element("summary", {"About"}), + preprocess_html(req, server, {}, std::move(extended_description)), + }), + + Element("details", {{"class", "about_page-rules"}}, { + Element("summary", {"Rules"}), + rules, + }), + }); + serve(req, res, "About "s + instance.title, std::move(body)); +} diff --git a/routes/css.cpp b/routes/css.cpp index c6c8c55..21d8423 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -26,7 +26,7 @@ p, details { margin-top: 1em; margin-bottom: 1em; } -ul { +ul, ol { margin: revert; padding: revert; } @@ -165,7 +165,6 @@ svg { .user_page-header { width: 100%; height: 15em; - object-fit: cover; } .user_page-main_header { @@ -237,6 +236,20 @@ svg { /* don't ask why, but making it a block element just works */ display: block; } + +/* ABOUT PAGE */ +.about_page-header { + width: 100%; + height: 15em; +} +:is(.about_page-about, .about_page-rules) summary { + font-size: 130%; + font-weight: bold; +} +.about_page-about * { + margin: revert; + padding: revert; +} )EOF"; // one for \0, one for trailing newline #define CSS_LEN sizeof(css) / sizeof(css[0]) - 2 diff --git a/routes/routes.h b/routes/routes.h index 45a5a76..a38898f 100644 --- a/routes/routes.h +++ b/routes/routes.h @@ -9,3 +9,4 @@ void css_route(const httplib::Request& req, httplib::Response& res); void user_route(const httplib::Request& req, httplib::Response& res); void status_route(const httplib::Request& req, httplib::Response& res); void tags_route(const httplib::Request& req, httplib::Response& res); +void about_route(const httplib::Request& req, httplib::Response& res);