#include "routes.h" #include "../servehelper.h" #include "../htmlhelper.h" #include "../client.h" #include "../models.h" #include "../timeutils.h" static const char* sorting_method_names[3] = {"Posts", "Posts and replies", "Media"}; static const char* sorting_method_suffixes[3] = {"", "/with_replies", "/media"}; static inline PostSortingMethod get_sorting_method(const std::string& method); static inline Element user_header(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod sorting_method); static inline Element user_link_field(const httplib::Request& req, const Account& account, const AccountField& field); static inline Element sorting_method_link(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod current_method, PostSortingMethod new_method); static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Account& account, const std::optional& max_id, PostSortingMethod sorting_method); void user_route(const httplib::Request& req, httplib::Response& res) { using namespace std::string_literals; std::string server = req.matches.str(1); std::string username = req.matches.str(2); PostSortingMethod sorting_method = get_sorting_method(req.matches.str(3)); std::optional max_id; if (req.has_param("max_id")) { max_id = req.get_param_value("max_id"); } std::optional account; std::vector pinned_posts, posts; try { account = mastodon_client.get_account_by_username(server, username); if (account) { if (sorting_method == PostSortingMethod::Posts && !max_id) { pinned_posts = mastodon_client.get_pinned_posts(server, account->id); } posts = mastodon_client.get_posts(server, account->id, sorting_method, max_id); } } catch (const std::exception& e) { res.status = 500; serve_error(req, res, "500: Internal server error", "Failed to fetch user information", e.what()); return; } if (!account) { res.status = 404; serve_error(req, res, "404: User not found"); return; } Element body("body", { user_header(req, server, *account, sorting_method), }); body.nodes.reserve(body.nodes.size() + 2 * (pinned_posts.size() + posts.size()) + 1); for (const Post& post : pinned_posts) { body.nodes.push_back(serialize_post(req, server, post, true)); body.nodes.push_back(Element("hr")); } for (const Post& post : posts) { body.nodes.push_back(serialize_post(req, server, post)); body.nodes.push_back(Element("hr")); } if (!posts.empty()) { body.nodes.push_back(Element("a", {{"class", "more_posts"}, {"href", "?max_id="s + posts[posts.size() - 1].id + "#user_posts_nav"}}, {"See more"})); } else if (max_id) { body.nodes.push_back(Element("p", {{"class", "more_posts"}}, {"There are no more posts"})); } serve(req, res, account->display_name + " (@" + account->acct() + ')', std::move(body), generate_ogp_nodes(req, *account, max_id, sorting_method)); } static inline PostSortingMethod get_sorting_method(const std::string& method) { for (size_t i = 0; i < sizeof(sorting_method_suffixes) / sizeof(sorting_method_suffixes[0]); i++) { if (method == sorting_method_suffixes[i]) { return static_cast(i); } } __builtin_unreachable(); } static inline Element user_header(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod sorting_method) { blankie::html::HTMLString preprocessed_bio = preprocess_html(req, account.server, account.emojis, account.note_html); // Workaround for https://social.kernel.org/@monsieuricon if (preprocessed_bio.str.find("

") == std::string::npos) { size_t offset = 0; while ((offset = preprocessed_bio.str.find('\n', offset)) != std::string::npos) { preprocessed_bio.str.replace(offset, 1, "
"); offset += 4; } preprocessed_bio.str.reserve(preprocessed_bio.str.size() + 3 + 4); preprocessed_bio.str.insert(0, "

"); preprocessed_bio.str.append("

"); } Element user_links("table", {{"class", "user_page-user_links"}}, {}); user_links.nodes.reserve(account.fields.size()); for (const AccountField& i : account.fields) { user_links.nodes.push_back(user_link_field(req, account, i)); } Node view_on_original = !account.same_server ? Element("span", {" (", Element("a", {{"href", get_origin(req) + '/' + account.server + "/@" + account.username}}, {"View on original instance"}), ")"}) : Node(""); Element header("header", { Element("a", {{"href", account.header}}, { Element("img", {{"class", "user_page-header"}, {"alt", "User header"}, {"loading", "lazy"}, {"src", account.header}}, {}), }), Element("div", {{"class", "user_page-main_header"}}, { Element("a", {{"href", account.avatar}}, { Element("img", {{"class", "user_page-profile"}, {"alt", "User profile picture"}, {"loading", "lazy"}, {"src", account.avatar}}, {}), }), Element("span", {{"class", "user_page-main_header_text"}}, { // left-to-right override--thank https://anarres.family/@alice@mk.nyaa.place Element("b", {preprocess_html(req, account.emojis, account.display_name)}), "\u202d", account.bot ? " (bot)" : "", " (@", account.acct(), ")", view_on_original, Element("br"), Element("br"), Element("b", {"Joined: "}), short_time(account.created_at), Element("br"), Element("b", {std::to_string(account.statuses_count)}), " Posts", " / ", Element("b", {std::to_string(account.following_count)}), " Following", " / ", // https://chaosfem.tw/@Terra Element("b", {account.followers_count >= 0 ? std::to_string(account.followers_count) : "???"}), " Followers", }), }), Element("div", {{"class", "user_page-user_description"}}, { Element("div", {{"class", "user_page-user_bio"}}, {std::move(preprocessed_bio)}), std::move(user_links), }), Element("nav", {{"class", "user_page-user_posts_nav"}, {"id", "user_posts_nav"}}, { sorting_method_link(req, server, account, sorting_method, PostSortingMethod::Posts), sorting_method_link(req, server, account, sorting_method, PostSortingMethod::PostsAndReplies), sorting_method_link(req, server, account, sorting_method, PostSortingMethod::MediaOnly), }), }); return header; } static inline Element user_link_field(const httplib::Request& req, const Account& account, const AccountField& field) { using namespace std::string_literals; Element tr("tr", { Element("th", {preprocess_html(req, account.emojis, field.name)}), Element("td", {preprocess_html(req, account.server, account.emojis, field.value_html)}), }); if (field.verified_at >= 0) { tr.attributes = {{"class", "verified"}, {"title", "Verified at "s + full_time(field.verified_at)}}; } return tr; } static inline Element sorting_method_link(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod current_method, PostSortingMethod new_method) { const char* method_name = sorting_method_names[new_method]; Element a("a", {{"href", get_origin(req) + '/' + server + "/@" + account.acct(false) + sorting_method_suffixes[new_method] + "#user_posts_nav"}}, { method_name, }); if (current_method == new_method) { a.attributes.push_back({"class", "selected"}); } return a; } static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Account& account, const std::optional& max_id, PostSortingMethod sorting_method) { std::string url = get_origin(req) + '/' + account.server + "/@" + account.acct(false) + sorting_method_suffixes[sorting_method]; if (max_id) { url += "?max_id="; url += *max_id; } std::string note = get_text_content(account.note_html); Nodes nodes({ // left-to-right override--thank https://anarres.family/@alice@mk.nyaa.place Element("meta", {{"property", "og:title"}, {"content", account.display_name + "\u202d (@" + account.acct() + ')'}}, {}), Element("meta", {{"property", "og:type"}, {"content", "website"}}, {}), Element("meta", {{"property", "og:site_name"}, {"content", "Coyote"}}, {}), Element("meta", {{"property", "og:url"}, {"content", std::move(url)}}, {}), Element("meta", {{"property", "og:image"}, {"content", account.avatar}}, {}), }); if (!note.empty()) { nodes.push_back(Element("meta", {{"property", "og:description"}, {"content", std::move(note)}}, {})); } return nodes; }