From 8635127ec3734f649eff753aa1d6897654fda42b Mon Sep 17 00:00:00 2001 From: blankie Date: Mon, 4 Dec 2023 14:48:21 +1100 Subject: [PATCH] Try to fetch account information from posts without one Example of one with: https://dlx.pink/notice/AbtdJkjioOo8ZSdDhw Example of one without: https://dlx.pink/notice/AbD2kgNviafFEsebqq --- models.cpp | 31 ++++++++++++++++++++++++------- routes/css.cpp | 7 ++++++- routes/status.cpp | 1 + servehelper.cpp | 22 ++++++++++++++++------ 4 files changed, 47 insertions(+), 14 deletions(-) diff --git a/models.cpp b/models.cpp index 6756bc8..895c0fa 100644 --- a/models.cpp +++ b/models.cpp @@ -7,6 +7,10 @@ #include "models.h" #include "numberhelper.h" +#define DOMAIN_RE "(?:[a-zA-Z0-9-]+\\.)+[a-zA-Z]{2,}" +// https://docs.joinmastodon.org/methods/accounts/#422-unprocessable-entity +#define USERNAME_RE "[a-zA-Z0-9_]+" + using json = nlohmann::json; static time_t parse_rfc3339(const std::string& str); @@ -28,15 +32,10 @@ void from_json(const json& j, AccountField& field) { } } -static std::regex host_regex(R"EOF(https?://([a-z0-9-.]+)/.+)EOF", std::regex::ECMAScript | std::regex::icase); +static std::regex host_regex("https?://(" DOMAIN_RE ")/.+", std::regex::ECMAScript | std::regex::icase); void from_json(const json& j, Account& account) { using namespace std::string_literals; - // https://dlx.pink/notice/AbtdJkjioOo8ZSdDhw - if (j.size() == 0) { - return; - } - j.at("id").get_to(account.id); j.at("username").get_to(account.username); j.at("display_name").get_to(account.display_name); @@ -117,6 +116,7 @@ void from_json(const json& j, Poll& poll) { j.at("emojis").get_to(poll.emojis); } +static std::regex akkoma_status_url_regex("https?://(" DOMAIN_RE ")/(?:@|users/)(" USERNAME_RE ")/.+"); void from_json(const json& j, Post& post) { j.at("id").get_to(post.id); post.created_at = parse_rfc3339(j.at("created_at").get_ref()); @@ -142,13 +142,30 @@ void from_json(const json& j, Post& post) { post.reblog = std::make_unique(); from_json(j["reblog"].get(), *post.reblog.get()); } - j.at("account").get_to(post.account); j.at("media_attachments").get_to(post.media_attachments); j.at("emojis").get_to(post.emojis); // https://social.kernel.org/@monsieuricon/Ac6oYwtLhess6uil1c if (j.contains("poll") && !j["poll"].is_null()) { post.poll = j["poll"].get(); } + + // empty account with username accessible: https://dlx.pink/notice/AbtdJkjioOo8ZSdDhw + // empty account with username inaccesible: https://dlx.pink/notice/AbD2kgNviafFEsebqq + if (j.at("account").size()) { + j["account"].get_to(post.account); + } else { + std::smatch sm; + const std::string& url = j.at("url").get_ref(); + if (!std::regex_match(url, sm, akkoma_status_url_regex)) { + return; + } + + post.account = { + .username = sm.str(2), + .server = sm.str(1), + .display_name = sm.str(2), + }; + } } void from_json(const json& j, PostContext& context) { diff --git a/routes/css.cpp b/routes/css.cpp index de2148e..3895f98 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -105,7 +105,12 @@ svg { .post-header span, .post-header time { margin-top: auto; margin-bottom: auto; - margin-left: 0.5em; +} +.post-header img { + margin-right: 0.5em; +} +.post-header time { + padding-left: 1em; } .post-time_header { margin-left: auto; diff --git a/routes/status.cpp b/routes/status.cpp index 4014d26..7bae3ef 100644 --- a/routes/status.cpp +++ b/routes/status.cpp @@ -33,6 +33,7 @@ void status_route(const httplib::Request& req, httplib::Response& res) { } // https://dlx.pink/notice/AbtdJkjioOo8ZSdDhw + // https://dlx.pink/notice/AbD2kgNviafFEsebqq if (post->reblog && !post->reblog->account.id.empty()) { serve_redirect(req, res, get_origin(req) + '/' + server + "/@" + post->reblog->account.acct(false) + '/' + post->reblog->id, true); return; diff --git a/servehelper.cpp b/servehelper.cpp index efbb9bd..f453c8c 100644 --- a/servehelper.cpp +++ b/servehelper.cpp @@ -26,7 +26,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, bool main_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, const Post* reblogged = nullptr); static inline Element serialize_media(const Media& media); static inline Element serialize_poll(const httplib::Request& req, const Poll& poll); @@ -177,7 +177,7 @@ 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, main_post, post_status); + return serialize_post(req, server, *post.reblog, main_post, post_status, &post); } else if (pinned) { PostStatus post_status = { fa_thumbtack, @@ -437,9 +437,17 @@ 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, bool main_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, const Post* reblogged) { using namespace std::string_literals; + bool user_known = !post.account.id.empty(); + bool user_ref_known = !post.account.username.empty() && !post.account.server.empty(); + // `reblogged == nullptr` since a malicious server could take down the frontend + // by sending a post that is not a reblog with no account information + std::string post_url = user_known || reblogged == nullptr + ? get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id + "#m" + : get_origin(req) + '/' + server + "/@" + reblogged->account.acct(false) + '/' + reblogged->id + "#m"; + std::string time_title = post.edited_at < 0 ? full_time(post.created_at) : "Created: "s + full_time(post.created_at) + "\nEdited: " + full_time(post.edited_at); @@ -478,14 +486,16 @@ static Element serialize_post(const httplib::Request& req, const std::string& se Element div("div", {{"class", "post"}}, { Element("div", {{"class", "post-header"}}, { - !post.account.id.empty() ? Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false)}}, { - Element("img", {{"class", "post-avatar"}, {"alt", "User profile picture"}, {"loading", "lazy"}, {"src", post.account.avatar_static}}, {}), + user_ref_known ? Element("a", {{"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false)}}, { + !post.account.avatar_static.empty() + ? Element("img", {{"class", "post-avatar"}, {"alt", "User profile picture"}, {"loading", "lazy"}, {"src", post.account.avatar_static}}, {}) + : Node(""), Element("span", { Element("b", {preprocess_html(req, post.account.emojis, post.account.display_name)}), Element("br"), "@", post.account.acct(), }), }) : Element("b", {"Unknown user"}), - Element("a", {{"class", "post-time_header"}, {"href", get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id + "#m"}, {"title", time_title}}, { + Element("a", {{"class", "post-time_header"}, {"href", std::move(post_url)}, {"title", time_title}}, { Element("time", {{"datetime", to_rfc3339(post.created_at)}}, {relative_time(post.created_at, current_time()), time_badge}), }), }),