From 452ac756e6b7da2612b18b75ab567fa93d6db87a Mon Sep 17 00:00:00 2001 From: blankie Date: Fri, 24 Nov 2023 10:46:47 +1100 Subject: [PATCH] Add post context support --- client.cpp | 71 ++++++++++++++++++++++++++++------------------- client.h | 4 ++- models.cpp | 5 ++++ models.h | 6 ++++ routes/css.cpp | 3 ++ routes/status.cpp | 20 +++++++++++-- servehelper.cpp | 21 ++++++++------ servehelper.h | 2 +- 8 files changed, 90 insertions(+), 42 deletions(-) diff --git a/client.cpp b/client.cpp index b3caf21..b156c37 100644 --- a/client.cpp +++ b/client.cpp @@ -7,6 +7,7 @@ MastodonClient mastodon_client; +static void handle_post_server(Post& post, const std::string& host); static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp); static void share_unlock(CURL* curl, curl_lock_data data, void* clientp); static size_t curl_write_cb(char* ptr, size_t size, size_t nmemb, void* userdata); @@ -82,26 +83,6 @@ std::optional MastodonClient::get_account_by_username(const std::string } } -std::optional MastodonClient::get_post(const std::string& host, const std::string& id) { - using namespace std::string_literals; - - try { - std::string resp = this->_send_request("https://"s + host + "/api/v1/statuses/" + id); - Post post = nlohmann::json::parse(std::move(resp)); - post.account.same_server = host == post.account.server; - if (post.reblog) { - post.reblog->account.same_server = host == post.reblog->account.server; - } - return post; - } catch (const CurlException& e) { - if (e.code != CURLE_HTTP_RETURNED_ERROR || this->_response_status_code() != 404) { - throw; - } - - return std::nullopt; - } -} - std::vector MastodonClient::get_pinned_posts(const std::string& host, const std::string& account_id) { using namespace std::string_literals; @@ -109,10 +90,7 @@ std::vector MastodonClient::get_pinned_posts(const std::string& host, cons std::vector posts = nlohmann::json::parse(std::move(resp)); for (Post& post : posts) { - post.account.same_server = host == post.account.server; - if (post.reblog) { - post.reblog->account.same_server = host == post.reblog->account.server; - } + handle_post_server(post, host); } return posts; @@ -140,15 +118,45 @@ std::vector MastodonClient::get_posts(const std::string& host, const std:: std::vector posts = nlohmann::json::parse(std::move(resp)); for (Post& post : posts) { - post.account.same_server = host == post.account.server; - if (post.reblog) { - post.reblog->account.same_server = host == post.reblog->account.server; - } + handle_post_server(post, host); } return posts; } +std::optional MastodonClient::get_post(const std::string& host, const std::string& id) { + using namespace std::string_literals; + + try { + std::string resp = this->_send_request("https://"s + host + "/api/v1/statuses/" + id); + Post post = nlohmann::json::parse(std::move(resp)); + handle_post_server(post, host); + return post; + } catch (const CurlException& e) { + if (e.code != CURLE_HTTP_RETURNED_ERROR || this->_response_status_code() != 404) { + throw; + } + + return std::nullopt; + } +} + +PostContext MastodonClient::get_post_context(const std::string& host, const std::string& id) { + using namespace std::string_literals; + + std::string resp = this->_send_request("https://"s + host + "/api/v1/statuses/" + id + "/context"); + PostContext context = nlohmann::json::parse(std::move(resp)); + + for (Post& post : context.ancestors) { + handle_post_server(post, host); + } + for (Post& post : context.descendants) { + handle_post_server(post, host); + } + + return context; +} + CURL* MastodonClient::_get_easy() { CURL* curl = pthread_getspecific(this->_easy_key); if (!curl) { @@ -203,6 +211,13 @@ long MastodonClient::_response_status_code() { } +static void handle_post_server(Post& post, const std::string& host) { + post.account.same_server = host == post.account.server; + if (post.reblog) { + post.reblog->account.same_server = host == post.reblog->account.server; + } +} + static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp) { (void)curl; (void)access; diff --git a/client.h b/client.h index 27703ea..a99fc4d 100644 --- a/client.h +++ b/client.h @@ -50,10 +50,12 @@ public: } std::optional get_account_by_username(const std::string& host, const std::string& username); - std::optional get_post(const std::string& host, const std::string& id); std::vector get_pinned_posts(const std::string& host, const std::string& account_id); std::vector get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional max_id); + std::optional get_post(const std::string& host, const std::string& id); + PostContext get_post_context(const std::string& host, const std::string& id); + private: CURL* _get_easy(); std::string _send_request(const std::string& url); diff --git a/models.cpp b/models.cpp index f07d9b9..f2ba2c9 100644 --- a/models.cpp +++ b/models.cpp @@ -95,6 +95,11 @@ void from_json(const json& j, Post& post) { j.at("emojis").get_to(post.emojis); } +void from_json(const json& j, PostContext& context) { + j.at("ancestors").get_to(context.ancestors); + j.at("descendants").get_to(context.descendants); +} + 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 ea11cf1..cd27c82 100644 --- a/models.h +++ b/models.h @@ -76,8 +76,14 @@ struct Post { std::vector emojis; }; +struct PostContext { + std::vector ancestors; + std::vector descendants; +}; + 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); void from_json(const nlohmann::json& j, Media& media); void from_json(const nlohmann::json& j, Post& post); +void from_json(const nlohmann::json& j, PostContext& context); diff --git a/routes/css.cpp b/routes/css.cpp index 9b7f856..e9409e6 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -62,6 +62,9 @@ svg { } /* POST */ +.main_post .post-contents { + font-size: 110%; +} .post { margin-top: 1em; } diff --git a/routes/status.cpp b/routes/status.cpp index 1b70402..dd898b1 100644 --- a/routes/status.cpp +++ b/routes/status.cpp @@ -8,8 +8,12 @@ void status_route(const httplib::Request& req, httplib::Response& res) { std::string id = req.matches.str(2); std::optional post; + PostContext context; try { post = mastodon_client.get_post(server, id); + if (post) { + context = mastodon_client.get_post_context(server, id); + } } catch (const std::exception& e) { res.status = 500; serve_error(req, res, "500: Internal server error", "Failed to fetch post information", e.what()); @@ -27,8 +31,18 @@ void status_route(const httplib::Request& req, httplib::Response& res) { return; } - Element body("body", { - serialize_post(req, server, *post), - }); + Element body("body"); + body.nodes.reserve(context.ancestors.size() * 2 + 1 + context.descendants.size() * 2); + + for (const Post& i : context.ancestors) { + body.nodes.push_back(serialize_post(req, server, i)); + body.nodes.push_back(Element("hr")); + } + body.nodes.push_back(serialize_post(req, server, *post, false, true)); + for (const Post& i : context.descendants) { + body.nodes.push_back(Element("hr")); + body.nodes.push_back(serialize_post(req, server, i)); + } + serve(req, res, "", std::move(body)); } diff --git a/servehelper.cpp b/servehelper.cpp index 41059ab..66590c9 100644 --- a/servehelper.cpp +++ b/servehelper.cpp @@ -23,7 +23,7 @@ struct PostStatus { const char* icon_html; Node info_node; }; -static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, const std::optional& post_status); +static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool main_post, const std::optional& post_status); static inline Element serialize_media(const Media& media); class CurlUrlException : public std::exception { @@ -172,7 +172,7 @@ bool should_send_304(const httplib::Request& req, uint64_t hash) { return pos != std::string::npos && (pos == 0 || header[pos - 1] != '/'); } -Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned) { +Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned, bool main_post) { using namespace std::string_literals; if (post.reblog) { @@ -180,21 +180,21 @@ Element serialize_post(const httplib::Request& req, const std::string& server, c fa_retweet, preprocess_html(req, post.account.emojis, post.account.display_name + " boosted"), }; - return serialize_post(req, server, *post.reblog, post_status); + return serialize_post(req, server, *post.reblog, main_post, post_status); } else if (pinned) { PostStatus post_status = { fa_thumbtack, blankie::html::HTMLString("Pinned post"), }; - return serialize_post(req, server, post, post_status); + return serialize_post(req, server, post, main_post, post_status); } else if (post.in_reply_to_id && post.in_reply_to_account_id && post.account.id == *post.in_reply_to_account_id) { PostStatus post_status = { fa_reply, preprocess_html(req, post.account.emojis, "Replied to "s + post.account.display_name), }; - return serialize_post(req, server, post, post_status); + return serialize_post(req, server, post, main_post, post_status); } else { - return serialize_post(req, server, post, std::nullopt); + return serialize_post(req, server, post, main_post, std::nullopt); } } @@ -361,7 +361,7 @@ static inline std::vector emojify(lxb_dom_document_t* document, return res; } -static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, const std::optional& post_status) { +static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool main_post, const std::optional& post_status) { using namespace std::string_literals; std::string time_title = post.edited_at < 0 @@ -369,7 +369,7 @@ static Element serialize_post(const httplib::Request& req, const std::string& se : "Created: "s + full_time(post.created_at) + "\nEdited: " + full_time(post.edited_at); const char* time_badge = post.edited_at < 0 ? "" : " (edited)"; - Element contents("div", {preprocess_html(req, server, post.emojis, post.content)}); + Element contents("div", {{"class", "post-contents"}}, {preprocess_html(req, server, post.emojis, post.content)}); Element post_attachments("div", {{"class", "post-attachments"}}, {}); post_attachments.nodes.reserve(post.media_attachments.size()); for (const Media& media : post.media_attachments) { @@ -394,7 +394,7 @@ static Element serialize_post(const httplib::Request& req, const std::string& se Element("br"), "@", post.account.acct(), }), }), - Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id}, {"title", time_title}}, { + Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id + "#m"}, {"title", time_title}}, { Element("time", {{"datetime", to_rfc3339(post.created_at)}}, {relative_time(post.created_at, current_time()), time_badge}), }), }), @@ -406,6 +406,9 @@ static Element serialize_post(const httplib::Request& req, const std::string& se blankie::html::HTMLString(post_status->icon_html), " ", post_status->info_node, })); } + if (main_post) { + div.attributes = {{"class", "post main_post"}, {"id", "m"}}; + } return div; } diff --git a/servehelper.h b/servehelper.h index fe48e40..d3448e4 100644 --- a/servehelper.h +++ b/servehelper.h @@ -20,7 +20,7 @@ std::string get_origin(const httplib::Request& req); std::string proxy_mastodon_url(const httplib::Request& req, const std::string& url_str); bool should_send_304(const httplib::Request& req, uint64_t hash); -Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned = false); +Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned = false, bool main_post = false); blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector& emojis, const blankie::html::HTMLString& str); blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector& emojis, const std::string& str);