#include "routes.h" #include "../lxb_wrapper.h" #include "../servehelper.h" #include "../client.h" #include "../models.h" static inline std::string make_title(const Post& post); static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Post& post, const std::string& server); static inline void generate_media_ogp_nodes(Nodes& nodes, const Media& media, bool* has_video, bool* has_image); void status_route(const httplib::Request& req, httplib::Response& res) { std::string server = req.matches.str(1); 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()); return; } if (!post) { res.status = 404; serve_error(req, res, "404: Post not found"); return; } if (post->reblog) { serve_redirect(req, res, get_origin(req) + '/' + server + "/@" + post->reblog->account.acct(false) + '/' + post->reblog->id, true); return; } 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, make_title(*post), std::move(body), generate_ogp_nodes(req, *post, server)); } static inline std::string make_title(const Post& post) { LXB::HTML::Document document(post.content.str); size_t content_len; const char* content = reinterpret_cast(lxb_dom_node_text_content(document.body(), &content_len)); std::string title = post.account.display_name + " (@" + post.account.acct() + "): "; if (content_len) { title.append(content, content_len > 50 ? 50 : content_len); if (content_len > 50) { title += "…"; } } else { title += "Post"; } return title; } static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Post& post, const std::string& server) { using namespace std::string_literals; std::string url = get_origin(req) + '/' + server + "/@" + post.account.acct(false) + '/' + post.id; bool has_video = false, has_image = false; Nodes nodes({ // left-to-right override--thank https://anarres.family/@alice@mk.nyaa.place Element("meta", {{"property", "og:title"}, {"content", post.account.display_name + "\u202d (@" + post.account.acct() + ')'}}, {}), Element("meta", {{"property", "og:site_name"}, {"content", "Coyote"}}, {}), Element("meta", {{"property", "og:url"}, {"content", std::move(url)}}, {}), }); if (!post.sensitive) { nodes.push_back(Element("meta", {{"property", "og:description"}, {"content", get_text_content(post.content)}}, {})); for (const Media& media : post.media_attachments) { generate_media_ogp_nodes(nodes, media, &has_video, &has_image); } } else if (!post.spoiler_text.empty()) { nodes.push_back(Element("meta", {{"property", "og:description"}, {"content", "CW: "s + post.spoiler_text}}, {})); } const char* type = !post.sensitive && has_video ? "video" : !post.sensitive && has_image ? "photo" : "article"; nodes.push_back(Element("meta", {{"property", "og:type"}, {"content", type}}, {})); return nodes; } static inline void generate_media_ogp_nodes(Nodes& nodes, const Media& media, bool* has_video, bool* has_image) { if (media.type == "image") { *has_image = true; nodes.push_back(Element("meta", {{"property", "og:image"}, {"content", media.preview_url.value_or(media.url)}}, {})); std::optional size = media.preview_size ? media.preview_size : media.size; if (size) { nodes.push_back(Element("meta", {{"property", "og:image:width"}, {"content", std::to_string(size->width)}}, {})); nodes.push_back(Element("meta", {{"property", "og:image:height"}, {"content", std::to_string(size->height)}}, {})); } if (media.description) { nodes.push_back(Element("meta", {{"property", "og:image:alt"}, {"content", *media.description}}, {})); } } else if (media.type == "video" || media.type == "gifv") { *has_video = true; nodes.push_back(Element("meta", {{"property", "og:video"}, {"content", media.preview_url.value_or(media.url)}}, {})); std::optional size = media.preview_size ? media.preview_size : media.size; if (size) { nodes.push_back(Element("meta", {{"property", "og:video:width"}, {"content", std::to_string(size->width)}}, {})); nodes.push_back(Element("meta", {{"property", "og:video:height"}, {"content", std::to_string(size->height)}}, {})); } if (media.description) { nodes.push_back(Element("meta", {{"property", "og:video:alt"}, {"content", *media.description}}, {})); } } else if (media.type == "audio") { nodes.push_back(Element("meta", {{"property", "og:audio"}, {"content", media.url}}, {})); } }