Split HTML helpers into their own file
This commit is contained in:
parent
d1711a040b
commit
973a0eada2
|
@ -29,7 +29,7 @@ list(APPEND FLAGS -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-s
|
|||
add_link_options(${FLAGS})
|
||||
|
||||
|
||||
add_executable(${PROJECT_NAME} main.cpp numberhelper.cpp hex.cpp config.cpp settings.cpp models.cpp client.cpp servehelper.cpp timeutils.cpp hiredis_wrapper.cpp
|
||||
add_executable(${PROJECT_NAME} main.cpp numberhelper.cpp hex.cpp config.cpp settings.cpp models.cpp client.cpp servehelper.cpp htmlhelper.cpp timeutils.cpp hiredis_wrapper.cpp
|
||||
routes/home.cpp routes/css.cpp routes/user.cpp routes/status.cpp routes/tags.cpp routes/about.cpp routes/user_settings.cpp
|
||||
blankie/serializer.cpp blankie/escape.cpp)
|
||||
set_target_properties(${PROJECT_NAME}
|
||||
|
|
|
@ -0,0 +1,456 @@
|
|||
#include "models.h"
|
||||
#include "settings.h"
|
||||
#include "timeutils.h"
|
||||
#include "curlu_wrapper.h"
|
||||
#include "font_awesome.h"
|
||||
#include "blankie/escape.h"
|
||||
|
||||
#include "htmlhelper.h"
|
||||
|
||||
static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, lxb_dom_element_t* element);
|
||||
static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element);
|
||||
static inline bool should_fix_link(lxb_dom_element_t* element, const std::string& element_cls);
|
||||
static inline void get_text_content(lxb_dom_node_t* node, std::string& out);
|
||||
static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector<Emoji>& emojis);
|
||||
static inline std::vector<lxb_dom_node*> emojify(lxb_dom_document_t* document, std::string str, const std::vector<Emoji>& emojis);
|
||||
|
||||
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<PostStatus>& 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);
|
||||
|
||||
|
||||
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) {
|
||||
PostStatus post_status = {
|
||||
fa_retweet,
|
||||
preprocess_html(req, post.account.emojis, post.account.display_name + " boosted"),
|
||||
};
|
||||
return serialize_post(req, server, *post.reblog, main_post, post_status, &post);
|
||||
} else if (pinned) {
|
||||
PostStatus post_status = {
|
||||
fa_thumbtack,
|
||||
blankie::html::HTMLString("Pinned post"),
|
||||
};
|
||||
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, main_post, post_status);
|
||||
} else {
|
||||
return serialize_post(req, server, post, main_post, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_text_content(lxb_dom_node_t* child) {
|
||||
std::string out;
|
||||
get_text_content(child, out);
|
||||
|
||||
if (!out.empty()) {
|
||||
size_t remove_from = out.size();
|
||||
while (remove_from && out[remove_from - 1] == '\n') {
|
||||
remove_from--;
|
||||
}
|
||||
// Don't engulf everything, otherwise it crashes
|
||||
// https://ruby.social/@CoralineAda/109951421922797743
|
||||
if (out.size() > remove_from && remove_from != 0) {
|
||||
out.erase(remove_from);
|
||||
}
|
||||
}
|
||||
if (!out.empty()) {
|
||||
size_t remove_to = 0;
|
||||
while (out.size() > remove_to && out[remove_to] == '\n') {
|
||||
remove_to++;
|
||||
}
|
||||
// Don't engulf everything, otherwise it crashes
|
||||
// https://ruby.social/@CoralineAda/109951421922797743
|
||||
if (out.size() > remove_to) {
|
||||
out.erase(0, remove_to);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string get_text_content(blankie::html::HTMLString str) {
|
||||
LXB::HTML::Document document(str.str);
|
||||
return get_text_content(document.body());
|
||||
}
|
||||
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str) {
|
||||
LXB::HTML::Document document(str.str);
|
||||
preprocess_html(req, domain_name, emojis, document.body_element());
|
||||
return blankie::html::HTMLString(document.serialize());
|
||||
}
|
||||
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str) {
|
||||
return preprocess_html(req, "", emojis, blankie::html::HTMLString(blankie::html::escape(str)));
|
||||
}
|
||||
|
||||
|
||||
static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, lxb_dom_element_t* element) {
|
||||
const char* tag_name = reinterpret_cast<const char*>(lxb_dom_element_tag_name(element, nullptr));
|
||||
|
||||
if (strncmp(tag_name, "A", 2) == 0) {
|
||||
// Proprocess links
|
||||
preprocess_link(req, domain_name, element);
|
||||
}
|
||||
|
||||
// Walk through the element's children
|
||||
lxb_dom_node_t* child = lxb_dom_node_first_child(lxb_dom_interface_node(element));
|
||||
while (child) {
|
||||
if (child->type == LXB_DOM_NODE_TYPE_ELEMENT) {
|
||||
preprocess_html(req, domain_name, emojis, lxb_dom_interface_element(child));
|
||||
} else if (child->type == LXB_DOM_NODE_TYPE_TEXT) {
|
||||
child = emojify(child, emojis);
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
}
|
||||
}
|
||||
|
||||
static std::regex mention_class_re("\\bmention\\b");
|
||||
static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
// Remove target=...
|
||||
lxb_status_t status = lxb_dom_element_remove_attribute(element, reinterpret_cast<const lxb_char_t*>("target"), 6);
|
||||
if (status != LXB_STATUS_OK) {
|
||||
throw LXB::Exception(status);
|
||||
}
|
||||
|
||||
size_t href_c_len;
|
||||
const lxb_char_t* href_c = lxb_dom_element_get_attribute(element, reinterpret_cast<const lxb_char_t*>("href"), 4, &href_c_len);
|
||||
if (!href_c) {
|
||||
return;
|
||||
}
|
||||
std::string href(reinterpret_cast<const char*>(href_c), href_c_len);
|
||||
|
||||
size_t cls_c_len;
|
||||
const lxb_char_t* cls_c = lxb_dom_element_class(element, &cls_c_len);
|
||||
std::string cls = cls_c ? std::string(reinterpret_cast<const char*>(cls_c), cls_c_len) : "";
|
||||
|
||||
try {
|
||||
CurlUrl href_url;
|
||||
href_url.set(CURLUPART_URL, get_origin(req));
|
||||
href_url.set(CURLUPART_PATH, std::string(href_url.get(CURLUPART_PATH).get()) + req.path);
|
||||
href_url.set(CURLUPART_URL, href);
|
||||
|
||||
CurlUrl instance_url_base;
|
||||
instance_url_base.set(CURLUPART_SCHEME, "https");
|
||||
instance_url_base.set(CURLUPART_HOST, domain_name);
|
||||
|
||||
// .mention is used in note and posts
|
||||
// Instance base is used for link fields
|
||||
if (std::regex_search(cls, mention_class_re) || starts_with(href_url, instance_url_base)) {
|
||||
// Proxy this instance's URLs to Coyote
|
||||
href = proxy_mastodon_url(req, std::move(href));
|
||||
|
||||
lxb_dom_element_set_attribute(element, reinterpret_cast<const lxb_char_t*>("href"), 4, reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
||||
}
|
||||
} catch (const CurlUrlException& e) {
|
||||
// example: <a href=""></a> on eldritch.cafe/about
|
||||
if (e.code != CURLUE_MALFORMED_INPUT) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (should_fix_link(element, cls)) {
|
||||
// Set the content of each <a> to its href
|
||||
status = lxb_dom_node_text_content_set(lxb_dom_interface_node(element), reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
||||
if (status != LXB_STATUS_OK) {
|
||||
throw LXB::Exception(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::regex unhandled_link_re("\\bunhandled-link\\b");
|
||||
static inline bool should_fix_link(lxb_dom_element_t* element, const std::string& element_cls) {
|
||||
// https://vt.social/@LucydiaLuminous/111448085044245037
|
||||
if (std::regex_search(element_cls, unhandled_link_re)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto expected_element = [](lxb_dom_node_t* node, const char* expected_cls) {
|
||||
if (!node || node->type != LXB_DOM_NODE_TYPE_ELEMENT) {
|
||||
return false;
|
||||
}
|
||||
lxb_dom_element_t* span = lxb_dom_interface_element(node);
|
||||
|
||||
const char* tag_name = reinterpret_cast<const char*>(lxb_dom_element_tag_name(span, nullptr));
|
||||
if (strncmp(tag_name, "SPAN", 5) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lxb_char_t* cls = lxb_dom_element_get_attribute(span, reinterpret_cast<const lxb_char_t*>("class"), 5, nullptr);
|
||||
return cls && strcmp(reinterpret_cast<const char*>(cls), expected_cls) == 0;
|
||||
};
|
||||
|
||||
lxb_dom_node_t* child = lxb_dom_node_first_child(lxb_dom_interface_node(element));
|
||||
if (!expected_element(child, "invisible")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
if (!expected_element(child, "ellipsis") && !expected_element(child, "")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
if (!expected_element(child, "invisible")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
return child == nullptr;
|
||||
}
|
||||
|
||||
static inline void get_text_content(lxb_dom_node_t* node, std::string& out) {
|
||||
bool is_br = false, is_p = false;
|
||||
|
||||
if (node->type == LXB_DOM_NODE_TYPE_TEXT) {
|
||||
size_t len;
|
||||
const char* text = reinterpret_cast<const char*>(lxb_dom_node_text_content(node, &len));
|
||||
|
||||
out.append(text, len);
|
||||
} else if (node->type == LXB_DOM_NODE_TYPE_ELEMENT) {
|
||||
lxb_dom_element_t* element = lxb_dom_interface_element(node);
|
||||
const char* tag_name = reinterpret_cast<const char*>(lxb_dom_element_tag_name(element, nullptr));
|
||||
|
||||
is_p = strncmp(tag_name, "P", 2) == 0;
|
||||
is_br = strncmp(tag_name, "BR", 3) == 0;
|
||||
}
|
||||
|
||||
if (is_p || is_br) {
|
||||
out.push_back('\n');
|
||||
}
|
||||
|
||||
lxb_dom_node_t* child = lxb_dom_node_first_child(node);
|
||||
while (child) {
|
||||
get_text_content(child, out);
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
}
|
||||
|
||||
if (is_p) {
|
||||
out.push_back('\n');
|
||||
}
|
||||
}
|
||||
|
||||
static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector<Emoji>& emojis) {
|
||||
std::vector<lxb_dom_node_t*> nodes = emojify(child->owner_document, get_text_content(child), emojis);
|
||||
|
||||
lxb_dom_node_insert_after(child, nodes[0]);
|
||||
lxb_dom_node_destroy(child);
|
||||
child = nodes[0];
|
||||
|
||||
for (size_t i = 1; i < nodes.size(); i++) {
|
||||
lxb_dom_node_insert_after(child, nodes[i]);
|
||||
child = nodes[i];
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
static std::regex shortcode_re(":([a-zA-Z0-9_]+):");
|
||||
static inline std::vector<lxb_dom_node_t*> emojify(lxb_dom_document_t* document, std::string str, const std::vector<Emoji>& emojis) {
|
||||
std::string buf;
|
||||
std::smatch sm;
|
||||
std::vector<lxb_dom_node*> res;
|
||||
|
||||
while (std::regex_search(str, sm, shortcode_re)) {
|
||||
buf += sm.prefix();
|
||||
|
||||
std::string group_0 = sm.str(0);
|
||||
auto emoji = std::find_if(emojis.begin(), emojis.end(), [&](const Emoji& i) { return i.shortcode == sm.str(1); });
|
||||
if (emoji != emojis.end()) {
|
||||
res.push_back(lxb_dom_interface_node(lxb_dom_document_create_text_node(document, reinterpret_cast<const lxb_char_t*>(buf.data()), buf.size())));
|
||||
buf.clear();
|
||||
|
||||
lxb_dom_element_t* img = lxb_dom_element_create(document, reinterpret_cast<const lxb_char_t*>("IMG"), 3, nullptr, 0, nullptr, 0, nullptr, 0, false);
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("class"), 5, reinterpret_cast<const lxb_char_t*>("custom_emoji"), 12);
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("alt"), 3, reinterpret_cast<const lxb_char_t*>(group_0.data()), group_0.size());
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("title"), 5, reinterpret_cast<const lxb_char_t*>(group_0.data()), group_0.size());
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("src"), 3, reinterpret_cast<const lxb_char_t*>(emoji->url.data()), emoji->url.size());
|
||||
res.push_back(lxb_dom_interface_node(img));
|
||||
} else {
|
||||
buf += group_0;
|
||||
}
|
||||
|
||||
str = sm.suffix();
|
||||
}
|
||||
|
||||
if (!str.empty()) {
|
||||
buf += std::move(str);
|
||||
}
|
||||
if (!buf.empty()) {
|
||||
res.push_back(lxb_dom_interface_node(lxb_dom_document_create_text_node(document, reinterpret_cast<const lxb_char_t*>(buf.data()), buf.size())));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool main_post, const std::optional<PostStatus>& 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);
|
||||
const char* time_badge = post.edited_at < 0 ? "" : " (edited)";
|
||||
|
||||
blankie::html::HTMLString preprocessed_html = preprocess_html(req, server, post.emojis, post.content);
|
||||
// Workaround for https://vt.social/@a1ba@suya.place/110552480243348878#m
|
||||
if (preprocessed_html.str.find("<p>") == std::string::npos) {
|
||||
preprocessed_html.str.reserve(preprocessed_html.str.size() + 3 + 4);
|
||||
preprocessed_html.str.insert(0, "<p>");
|
||||
preprocessed_html.str.append("</p>");
|
||||
}
|
||||
Element contents("div", {{"class", "post-contents"}}, {std::move(preprocessed_html)});
|
||||
|
||||
Element post_attachments("div", {{"class", "post-attachments"}}, {});
|
||||
post_attachments.nodes.reserve(post.media_attachments.size());
|
||||
for (const Media& media : post.media_attachments) {
|
||||
post_attachments.nodes.push_back(serialize_media(media));
|
||||
}
|
||||
contents.nodes.push_back(std::move(post_attachments));
|
||||
|
||||
if (post.poll) {
|
||||
contents.nodes.push_back(serialize_poll(req, *post.poll));
|
||||
}
|
||||
|
||||
if (post.sensitive) {
|
||||
std::string spoiler_text = !post.spoiler_text.empty() ? post.spoiler_text : "See more";
|
||||
contents = Element("details", {
|
||||
Element("summary", {preprocess_html(req, post.emojis, std::move(spoiler_text))}),
|
||||
std::move(contents),
|
||||
});
|
||||
if (UserSettings(req).auto_open_cw) {
|
||||
contents.attributes.push_back({"open", ""});
|
||||
}
|
||||
}
|
||||
|
||||
Element div("div", {{"class", "post"}}, {
|
||||
Element("div", {{"class", "post-header"}}, {
|
||||
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", std::move(post_url)}, {"title", time_title}}, {
|
||||
Element("time", {{"datetime", to_rfc3339(post.created_at)}}, {relative_time(post.created_at, current_time()), time_badge}),
|
||||
}),
|
||||
}),
|
||||
|
||||
contents,
|
||||
});
|
||||
if (post_status) {
|
||||
div.nodes.insert(div.nodes.begin(), Element("p", {
|
||||
blankie::html::HTMLString(post_status->icon_html), " ", post_status->info_node,
|
||||
}));
|
||||
}
|
||||
if (main_post) {
|
||||
div.attributes = {{"class", "post main_post"}, {"id", "m"}};
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
static inline Element serialize_media(const Media& media) {
|
||||
Element element = [&]() {
|
||||
if (media.type == "image") {
|
||||
return Element("a", {{"href", media.url}}, {
|
||||
Element("img", {{"loading", "lazy"}, {"src", media.preview_url.value_or(media.url)}}, {}),
|
||||
});
|
||||
} else if (media.type == "video") {
|
||||
Element video("video", {{"controls", ""}, {"src", media.url}}, {});
|
||||
if (media.preview_url) {
|
||||
video.attributes.push_back({"poster", *media.preview_url});
|
||||
}
|
||||
return video;
|
||||
} else if (media.type == "audio") {
|
||||
return Element("audio", {{"controls", ""}, {"src", media.url}}, {});
|
||||
} else if (media.type == "gifv") {
|
||||
// https://hachyderm.io/@Impossible_PhD/111444541628207638
|
||||
Element video("video", {{"controls", ""}, {"loop", ""}, {"muted", ""}, {"autoplay", ""}, {"src", media.url}}, {});
|
||||
if (media.preview_url) {
|
||||
video.attributes.push_back({"poster", *media.preview_url});
|
||||
}
|
||||
return video;
|
||||
} else if (media.type == "unknown") {
|
||||
if (media.remote_url) {
|
||||
// https://botsin.space/@lina@vt.social/111053598696451525
|
||||
return Element("a", {{"class", "unknown_media"}, {"href", *media.remote_url}}, {"Media is not available from this instance, view externally"});
|
||||
} else {
|
||||
return Element("p", {{"class", "unknown_media"}}, {"Media is not available from this instance"});
|
||||
}
|
||||
} else {
|
||||
return Element("p", {"Unsupported media type: ", media.type});
|
||||
}
|
||||
}();
|
||||
|
||||
if (media.description) {
|
||||
element.attributes.push_back({"alt", *media.description});
|
||||
element.attributes.push_back({"title", *media.description});
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
static inline Element serialize_poll(const httplib::Request& req, const Poll& poll) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
uint64_t voters_count = poll.voters_count >= 0 ? static_cast<uint64_t>(poll.voters_count) : poll.votes_count;
|
||||
Element div("div");
|
||||
|
||||
auto pick_form = [](uint64_t count, const char* singular, const char* plural) {
|
||||
return count == 1 ? singular : plural;
|
||||
};
|
||||
|
||||
div.nodes.reserve(poll.options.size() + 1);
|
||||
for (const PollOption& option : poll.options) {
|
||||
std::string percentage = voters_count
|
||||
? std::to_string(option.votes_count * 100 / voters_count) + '%'
|
||||
: "0%";
|
||||
|
||||
div.nodes.push_back(Element("div", {{"class", "poll-option"}, {"title", std::to_string(option.votes_count) + pick_form(option.votes_count, " vote", " votes")}}, {
|
||||
Element("b", {{"class", "poll-percentage"}}, {percentage}), " ", preprocess_html(req, poll.emojis, option.title),
|
||||
Element("object", {{"class", "poll-bar"}, {"width", percentage}}, {}),
|
||||
}));
|
||||
}
|
||||
|
||||
Element p("p", poll.voters_count >= 0
|
||||
? std::vector<Node>({std::to_string(voters_count), " ", pick_form(voters_count, "voter", "voters")})
|
||||
: std::vector<Node>({std::to_string(poll.votes_count), " ", pick_form(poll.votes_count, "vote", "votes")})
|
||||
);
|
||||
if (poll.expired) {
|
||||
p.nodes.push_back(" / ");
|
||||
p.nodes.push_back(Element("time", {{"datetime", to_rfc3339(poll.expires_at)}, {"title", "Expired on "s + full_time(poll.expires_at)}}, {"Expired"}));
|
||||
} else if (poll.expires_at >= 0) {
|
||||
p.nodes.push_back(" / ");
|
||||
p.nodes.push_back(Element("time", {{"datetime", to_rfc3339(poll.expires_at)}, {"title", full_time(poll.expires_at)}}, {
|
||||
"Expires in ", relative_time(current_time(), poll.expires_at),
|
||||
}));
|
||||
}
|
||||
div.nodes.push_back(std::move(p));
|
||||
|
||||
return div;
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
#pragma once
|
||||
|
||||
#include "lxb_wrapper.h"
|
||||
|
||||
#include "servehelper.h"
|
||||
struct Post; // forward declaration from models.h
|
||||
struct Emoji; // forward declaration from models.h
|
||||
|
||||
Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool pinned = false, bool main_post = false);
|
||||
|
||||
std::string get_text_content(lxb_dom_node_t* child);
|
||||
std::string get_text_content(blankie::html::HTMLString str);
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str);
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str);
|
1
models.h
1
models.h
|
@ -3,6 +3,7 @@
|
|||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
||||
#include "blankie/serializer.h"
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "routes.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../htmlhelper.h"
|
||||
#include "../client.h"
|
||||
#include "../models.h"
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include "routes.h"
|
||||
#include "../lxb_wrapper.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../htmlhelper.h"
|
||||
#include "../client.h"
|
||||
#include "../models.h"
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "routes.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../htmlhelper.h"
|
||||
#include "../client.h"
|
||||
#include "../models.h"
|
||||
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
#include "routes.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../htmlhelper.h"
|
||||
#include "../client.h"
|
||||
#include "../models.h"
|
||||
#include "../timeutils.h"
|
||||
|
|
454
servehelper.cpp
454
servehelper.cpp
|
@ -4,32 +4,10 @@
|
|||
#include <FastHash.h>
|
||||
#include <curl/curl.h>
|
||||
|
||||
#include "font_awesome.h"
|
||||
#include "config.h"
|
||||
#include "settings.h"
|
||||
#include "models.h"
|
||||
#include "timeutils.h"
|
||||
#include "servehelper.h"
|
||||
#include "lxb_wrapper.h"
|
||||
#include "curlu_wrapper.h"
|
||||
#include "routes/routes.h"
|
||||
#include "blankie/escape.h"
|
||||
|
||||
static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, lxb_dom_element_t* element);
|
||||
static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element);
|
||||
static inline bool should_fix_link(lxb_dom_element_t* element, const std::string& element_cls);
|
||||
static inline void get_text_content(lxb_dom_node_t* node, std::string& out);
|
||||
static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector<Emoji>& emojis);
|
||||
static inline std::vector<lxb_dom_node*> emojify(lxb_dom_document_t* document, std::string str, const std::vector<Emoji>& emojis);
|
||||
|
||||
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<PostStatus>& 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);
|
||||
|
||||
|
||||
void serve(const httplib::Request& req, httplib::Response& res, std::string title, Element element, Nodes extra_head) {
|
||||
using namespace std::string_literals;
|
||||
|
@ -168,435 +146,3 @@ bool should_send_304(const httplib::Request& req, uint64_t hash) {
|
|||
size_t pos = header.find(std::string(1, '"') + std::to_string(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, bool main_post) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
if (post.reblog) {
|
||||
PostStatus post_status = {
|
||||
fa_retweet,
|
||||
preprocess_html(req, post.account.emojis, post.account.display_name + " boosted"),
|
||||
};
|
||||
return serialize_post(req, server, *post.reblog, main_post, post_status, &post);
|
||||
} else if (pinned) {
|
||||
PostStatus post_status = {
|
||||
fa_thumbtack,
|
||||
blankie::html::HTMLString("Pinned post"),
|
||||
};
|
||||
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, main_post, post_status);
|
||||
} else {
|
||||
return serialize_post(req, server, post, main_post, std::nullopt);
|
||||
}
|
||||
}
|
||||
|
||||
std::string get_text_content(lxb_dom_node_t* child) {
|
||||
std::string out;
|
||||
get_text_content(child, out);
|
||||
|
||||
if (!out.empty()) {
|
||||
size_t remove_from = out.size();
|
||||
while (remove_from && out[remove_from - 1] == '\n') {
|
||||
remove_from--;
|
||||
}
|
||||
// Don't engulf everything, otherwise it crashes
|
||||
// https://ruby.social/@CoralineAda/109951421922797743
|
||||
if (out.size() > remove_from && remove_from != 0) {
|
||||
out.erase(remove_from);
|
||||
}
|
||||
}
|
||||
if (!out.empty()) {
|
||||
size_t remove_to = 0;
|
||||
while (out.size() > remove_to && out[remove_to] == '\n') {
|
||||
remove_to++;
|
||||
}
|
||||
// Don't engulf everything, otherwise it crashes
|
||||
// https://ruby.social/@CoralineAda/109951421922797743
|
||||
if (out.size() > remove_to) {
|
||||
out.erase(0, remove_to);
|
||||
}
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string get_text_content(blankie::html::HTMLString str) {
|
||||
LXB::HTML::Document document(str.str);
|
||||
return get_text_content(document.body());
|
||||
}
|
||||
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str) {
|
||||
LXB::HTML::Document document(str.str);
|
||||
preprocess_html(req, domain_name, emojis, document.body_element());
|
||||
return blankie::html::HTMLString(document.serialize());
|
||||
}
|
||||
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str) {
|
||||
return preprocess_html(req, "", emojis, blankie::html::HTMLString(blankie::html::escape(str)));
|
||||
}
|
||||
|
||||
|
||||
static inline void preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, lxb_dom_element_t* element) {
|
||||
const char* tag_name = reinterpret_cast<const char*>(lxb_dom_element_tag_name(element, nullptr));
|
||||
|
||||
if (strncmp(tag_name, "A", 2) == 0) {
|
||||
// Proprocess links
|
||||
preprocess_link(req, domain_name, element);
|
||||
}
|
||||
|
||||
// Walk through the element's children
|
||||
lxb_dom_node_t* child = lxb_dom_node_first_child(lxb_dom_interface_node(element));
|
||||
while (child) {
|
||||
if (child->type == LXB_DOM_NODE_TYPE_ELEMENT) {
|
||||
preprocess_html(req, domain_name, emojis, lxb_dom_interface_element(child));
|
||||
} else if (child->type == LXB_DOM_NODE_TYPE_TEXT) {
|
||||
child = emojify(child, emojis);
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
}
|
||||
}
|
||||
|
||||
static std::regex mention_class_re("\\bmention\\b");
|
||||
static inline void preprocess_link(const httplib::Request& req, const std::string& domain_name, lxb_dom_element_t* element) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
// Remove target=...
|
||||
lxb_status_t status = lxb_dom_element_remove_attribute(element, reinterpret_cast<const lxb_char_t*>("target"), 6);
|
||||
if (status != LXB_STATUS_OK) {
|
||||
throw LXB::Exception(status);
|
||||
}
|
||||
|
||||
size_t href_c_len;
|
||||
const lxb_char_t* href_c = lxb_dom_element_get_attribute(element, reinterpret_cast<const lxb_char_t*>("href"), 4, &href_c_len);
|
||||
if (!href_c) {
|
||||
return;
|
||||
}
|
||||
std::string href(reinterpret_cast<const char*>(href_c), href_c_len);
|
||||
|
||||
size_t cls_c_len;
|
||||
const lxb_char_t* cls_c = lxb_dom_element_class(element, &cls_c_len);
|
||||
std::string cls = cls_c ? std::string(reinterpret_cast<const char*>(cls_c), cls_c_len) : "";
|
||||
|
||||
try {
|
||||
CurlUrl href_url;
|
||||
href_url.set(CURLUPART_URL, get_origin(req));
|
||||
href_url.set(CURLUPART_PATH, std::string(href_url.get(CURLUPART_PATH).get()) + req.path);
|
||||
href_url.set(CURLUPART_URL, href);
|
||||
|
||||
CurlUrl instance_url_base;
|
||||
instance_url_base.set(CURLUPART_SCHEME, "https");
|
||||
instance_url_base.set(CURLUPART_HOST, domain_name);
|
||||
|
||||
// .mention is used in note and posts
|
||||
// Instance base is used for link fields
|
||||
if (std::regex_search(cls, mention_class_re) || starts_with(href_url, instance_url_base)) {
|
||||
// Proxy this instance's URLs to Coyote
|
||||
href = proxy_mastodon_url(req, std::move(href));
|
||||
|
||||
lxb_dom_element_set_attribute(element, reinterpret_cast<const lxb_char_t*>("href"), 4, reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
||||
}
|
||||
} catch (const CurlUrlException& e) {
|
||||
// example: <a href=""></a> on eldritch.cafe/about
|
||||
if (e.code != CURLUE_MALFORMED_INPUT) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
if (should_fix_link(element, cls)) {
|
||||
// Set the content of each <a> to its href
|
||||
status = lxb_dom_node_text_content_set(lxb_dom_interface_node(element), reinterpret_cast<const lxb_char_t*>(href.data()), href.size());
|
||||
if (status != LXB_STATUS_OK) {
|
||||
throw LXB::Exception(status);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static std::regex unhandled_link_re("\\bunhandled-link\\b");
|
||||
static inline bool should_fix_link(lxb_dom_element_t* element, const std::string& element_cls) {
|
||||
// https://vt.social/@LucydiaLuminous/111448085044245037
|
||||
if (std::regex_search(element_cls, unhandled_link_re)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
auto expected_element = [](lxb_dom_node_t* node, const char* expected_cls) {
|
||||
if (!node || node->type != LXB_DOM_NODE_TYPE_ELEMENT) {
|
||||
return false;
|
||||
}
|
||||
lxb_dom_element_t* span = lxb_dom_interface_element(node);
|
||||
|
||||
const char* tag_name = reinterpret_cast<const char*>(lxb_dom_element_tag_name(span, nullptr));
|
||||
if (strncmp(tag_name, "SPAN", 5) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const lxb_char_t* cls = lxb_dom_element_get_attribute(span, reinterpret_cast<const lxb_char_t*>("class"), 5, nullptr);
|
||||
return cls && strcmp(reinterpret_cast<const char*>(cls), expected_cls) == 0;
|
||||
};
|
||||
|
||||
lxb_dom_node_t* child = lxb_dom_node_first_child(lxb_dom_interface_node(element));
|
||||
if (!expected_element(child, "invisible")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
if (!expected_element(child, "ellipsis") && !expected_element(child, "")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
if (!expected_element(child, "invisible")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
return child == nullptr;
|
||||
}
|
||||
|
||||
static inline void get_text_content(lxb_dom_node_t* node, std::string& out) {
|
||||
bool is_br = false, is_p = false;
|
||||
|
||||
if (node->type == LXB_DOM_NODE_TYPE_TEXT) {
|
||||
size_t len;
|
||||
const char* text = reinterpret_cast<const char*>(lxb_dom_node_text_content(node, &len));
|
||||
|
||||
out.append(text, len);
|
||||
} else if (node->type == LXB_DOM_NODE_TYPE_ELEMENT) {
|
||||
lxb_dom_element_t* element = lxb_dom_interface_element(node);
|
||||
const char* tag_name = reinterpret_cast<const char*>(lxb_dom_element_tag_name(element, nullptr));
|
||||
|
||||
is_p = strncmp(tag_name, "P", 2) == 0;
|
||||
is_br = strncmp(tag_name, "BR", 3) == 0;
|
||||
}
|
||||
|
||||
if (is_p || is_br) {
|
||||
out.push_back('\n');
|
||||
}
|
||||
|
||||
lxb_dom_node_t* child = lxb_dom_node_first_child(node);
|
||||
while (child) {
|
||||
get_text_content(child, out);
|
||||
|
||||
child = lxb_dom_node_next(child);
|
||||
}
|
||||
|
||||
if (is_p) {
|
||||
out.push_back('\n');
|
||||
}
|
||||
}
|
||||
|
||||
static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector<Emoji>& emojis) {
|
||||
std::vector<lxb_dom_node_t*> nodes = emojify(child->owner_document, get_text_content(child), emojis);
|
||||
|
||||
lxb_dom_node_insert_after(child, nodes[0]);
|
||||
lxb_dom_node_destroy(child);
|
||||
child = nodes[0];
|
||||
|
||||
for (size_t i = 1; i < nodes.size(); i++) {
|
||||
lxb_dom_node_insert_after(child, nodes[i]);
|
||||
child = nodes[i];
|
||||
}
|
||||
|
||||
return child;
|
||||
}
|
||||
|
||||
static std::regex shortcode_re(":([a-zA-Z0-9_]+):");
|
||||
static inline std::vector<lxb_dom_node_t*> emojify(lxb_dom_document_t* document, std::string str, const std::vector<Emoji>& emojis) {
|
||||
std::string buf;
|
||||
std::smatch sm;
|
||||
std::vector<lxb_dom_node*> res;
|
||||
|
||||
while (std::regex_search(str, sm, shortcode_re)) {
|
||||
buf += sm.prefix();
|
||||
|
||||
std::string group_0 = sm.str(0);
|
||||
auto emoji = std::find_if(emojis.begin(), emojis.end(), [&](const Emoji& i) { return i.shortcode == sm.str(1); });
|
||||
if (emoji != emojis.end()) {
|
||||
res.push_back(lxb_dom_interface_node(lxb_dom_document_create_text_node(document, reinterpret_cast<const lxb_char_t*>(buf.data()), buf.size())));
|
||||
buf.clear();
|
||||
|
||||
lxb_dom_element_t* img = lxb_dom_element_create(document, reinterpret_cast<const lxb_char_t*>("IMG"), 3, nullptr, 0, nullptr, 0, nullptr, 0, false);
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("class"), 5, reinterpret_cast<const lxb_char_t*>("custom_emoji"), 12);
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("alt"), 3, reinterpret_cast<const lxb_char_t*>(group_0.data()), group_0.size());
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("title"), 5, reinterpret_cast<const lxb_char_t*>(group_0.data()), group_0.size());
|
||||
lxb_dom_element_set_attribute(img, reinterpret_cast<const lxb_char_t*>("src"), 3, reinterpret_cast<const lxb_char_t*>(emoji->url.data()), emoji->url.size());
|
||||
res.push_back(lxb_dom_interface_node(img));
|
||||
} else {
|
||||
buf += group_0;
|
||||
}
|
||||
|
||||
str = sm.suffix();
|
||||
}
|
||||
|
||||
if (!str.empty()) {
|
||||
buf += std::move(str);
|
||||
}
|
||||
if (!buf.empty()) {
|
||||
res.push_back(lxb_dom_interface_node(lxb_dom_document_create_text_node(document, reinterpret_cast<const lxb_char_t*>(buf.data()), buf.size())));
|
||||
}
|
||||
|
||||
return res;
|
||||
}
|
||||
|
||||
static Element serialize_post(const httplib::Request& req, const std::string& server, const Post& post, bool main_post, const std::optional<PostStatus>& 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);
|
||||
const char* time_badge = post.edited_at < 0 ? "" : " (edited)";
|
||||
|
||||
blankie::html::HTMLString preprocessed_html = preprocess_html(req, server, post.emojis, post.content);
|
||||
// Workaround for https://vt.social/@a1ba@suya.place/110552480243348878#m
|
||||
if (preprocessed_html.str.find("<p>") == std::string::npos) {
|
||||
preprocessed_html.str.reserve(preprocessed_html.str.size() + 3 + 4);
|
||||
preprocessed_html.str.insert(0, "<p>");
|
||||
preprocessed_html.str.append("</p>");
|
||||
}
|
||||
Element contents("div", {{"class", "post-contents"}}, {std::move(preprocessed_html)});
|
||||
|
||||
Element post_attachments("div", {{"class", "post-attachments"}}, {});
|
||||
post_attachments.nodes.reserve(post.media_attachments.size());
|
||||
for (const Media& media : post.media_attachments) {
|
||||
post_attachments.nodes.push_back(serialize_media(media));
|
||||
}
|
||||
contents.nodes.push_back(std::move(post_attachments));
|
||||
|
||||
if (post.poll) {
|
||||
contents.nodes.push_back(serialize_poll(req, *post.poll));
|
||||
}
|
||||
|
||||
if (post.sensitive) {
|
||||
std::string spoiler_text = !post.spoiler_text.empty() ? post.spoiler_text : "See more";
|
||||
contents = Element("details", {
|
||||
Element("summary", {preprocess_html(req, post.emojis, std::move(spoiler_text))}),
|
||||
std::move(contents),
|
||||
});
|
||||
if (UserSettings(req).auto_open_cw) {
|
||||
contents.attributes.push_back({"open", ""});
|
||||
}
|
||||
}
|
||||
|
||||
Element div("div", {{"class", "post"}}, {
|
||||
Element("div", {{"class", "post-header"}}, {
|
||||
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", std::move(post_url)}, {"title", time_title}}, {
|
||||
Element("time", {{"datetime", to_rfc3339(post.created_at)}}, {relative_time(post.created_at, current_time()), time_badge}),
|
||||
}),
|
||||
}),
|
||||
|
||||
contents,
|
||||
});
|
||||
if (post_status) {
|
||||
div.nodes.insert(div.nodes.begin(), Element("p", {
|
||||
blankie::html::HTMLString(post_status->icon_html), " ", post_status->info_node,
|
||||
}));
|
||||
}
|
||||
if (main_post) {
|
||||
div.attributes = {{"class", "post main_post"}, {"id", "m"}};
|
||||
}
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
static inline Element serialize_media(const Media& media) {
|
||||
Element element = [&]() {
|
||||
if (media.type == "image") {
|
||||
return Element("a", {{"href", media.url}}, {
|
||||
Element("img", {{"loading", "lazy"}, {"src", media.preview_url.value_or(media.url)}}, {}),
|
||||
});
|
||||
} else if (media.type == "video") {
|
||||
Element video("video", {{"controls", ""}, {"src", media.url}}, {});
|
||||
if (media.preview_url) {
|
||||
video.attributes.push_back({"poster", *media.preview_url});
|
||||
}
|
||||
return video;
|
||||
} else if (media.type == "audio") {
|
||||
return Element("audio", {{"controls", ""}, {"src", media.url}}, {});
|
||||
} else if (media.type == "gifv") {
|
||||
// https://hachyderm.io/@Impossible_PhD/111444541628207638
|
||||
Element video("video", {{"controls", ""}, {"loop", ""}, {"muted", ""}, {"autoplay", ""}, {"src", media.url}}, {});
|
||||
if (media.preview_url) {
|
||||
video.attributes.push_back({"poster", *media.preview_url});
|
||||
}
|
||||
return video;
|
||||
} else if (media.type == "unknown") {
|
||||
if (media.remote_url) {
|
||||
// https://botsin.space/@lina@vt.social/111053598696451525
|
||||
return Element("a", {{"class", "unknown_media"}, {"href", *media.remote_url}}, {"Media is not available from this instance, view externally"});
|
||||
} else {
|
||||
return Element("p", {{"class", "unknown_media"}}, {"Media is not available from this instance"});
|
||||
}
|
||||
} else {
|
||||
return Element("p", {"Unsupported media type: ", media.type});
|
||||
}
|
||||
}();
|
||||
|
||||
if (media.description) {
|
||||
element.attributes.push_back({"alt", *media.description});
|
||||
element.attributes.push_back({"title", *media.description});
|
||||
}
|
||||
|
||||
return element;
|
||||
}
|
||||
|
||||
static inline Element serialize_poll(const httplib::Request& req, const Poll& poll) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
uint64_t voters_count = poll.voters_count >= 0 ? static_cast<uint64_t>(poll.voters_count) : poll.votes_count;
|
||||
Element div("div");
|
||||
|
||||
auto pick_form = [](uint64_t count, const char* singular, const char* plural) {
|
||||
return count == 1 ? singular : plural;
|
||||
};
|
||||
|
||||
div.nodes.reserve(poll.options.size() + 1);
|
||||
for (const PollOption& option : poll.options) {
|
||||
std::string percentage = voters_count
|
||||
? std::to_string(option.votes_count * 100 / voters_count) + '%'
|
||||
: "0%";
|
||||
|
||||
div.nodes.push_back(Element("div", {{"class", "poll-option"}, {"title", std::to_string(option.votes_count) + pick_form(option.votes_count, " vote", " votes")}}, {
|
||||
Element("b", {{"class", "poll-percentage"}}, {percentage}), " ", preprocess_html(req, poll.emojis, option.title),
|
||||
Element("object", {{"class", "poll-bar"}, {"width", percentage}}, {}),
|
||||
}));
|
||||
}
|
||||
|
||||
Element p("p", poll.voters_count >= 0
|
||||
? std::vector<Node>({std::to_string(voters_count), " ", pick_form(voters_count, "voter", "voters")})
|
||||
: std::vector<Node>({std::to_string(poll.votes_count), " ", pick_form(poll.votes_count, "vote", "votes")})
|
||||
);
|
||||
if (poll.expired) {
|
||||
p.nodes.push_back(" / ");
|
||||
p.nodes.push_back(Element("time", {{"datetime", to_rfc3339(poll.expires_at)}, {"title", "Expired on "s + full_time(poll.expires_at)}}, {"Expired"}));
|
||||
} else if (poll.expires_at >= 0) {
|
||||
p.nodes.push_back(" / ");
|
||||
p.nodes.push_back(Element("time", {{"datetime", to_rfc3339(poll.expires_at)}, {"title", full_time(poll.expires_at)}}, {
|
||||
"Expires in ", relative_time(current_time(), poll.expires_at),
|
||||
}));
|
||||
}
|
||||
div.nodes.push_back(std::move(p));
|
||||
|
||||
return div;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,6 @@
|
|||
|
||||
#include <httplib/httplib.h>
|
||||
#include "blankie/serializer.h"
|
||||
#include "lxb_wrapper.h"
|
||||
struct Post; // forward declaration from models.h
|
||||
struct Emoji; // forward declaration from models.h
|
||||
class CurlUrl; // forward declaration from curlu_wrapper.h
|
||||
|
||||
using Element = blankie::html::Element;
|
||||
|
@ -22,10 +19,3 @@ bool starts_with(const CurlUrl& url, const CurlUrl& base);
|
|||
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, bool main_post = false);
|
||||
|
||||
std::string get_text_content(lxb_dom_node_t* child);
|
||||
std::string get_text_content(blankie::html::HTMLString str);
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::string& domain_name, const std::vector<Emoji>& emojis, const blankie::html::HTMLString& str);
|
||||
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str);
|
||||
|
|
Loading…
Reference in New Issue