Compare commits
3 Commits
278deff6da
...
6cb53f1bc1
Author | SHA1 | Date |
---|---|---|
blankie | 6cb53f1bc1 | |
blankie | dfe629aa2b | |
blankie | f6908ee486 |
|
@ -58,5 +58,6 @@ static inline bool is_autoclosing_tag(const char* tag) {
|
||||||
|| !strncmp(tag, "meta", 5)
|
|| !strncmp(tag, "meta", 5)
|
||||||
|| !strncmp(tag, "img", 4)
|
|| !strncmp(tag, "img", 4)
|
||||||
|| !strncmp(tag, "br", 3)
|
|| !strncmp(tag, "br", 3)
|
||||||
|
|| !strncmp(tag, "hr", 3)
|
||||||
|| !strncmp(tag, "input", 6);
|
|| !strncmp(tag, "input", 6);
|
||||||
}
|
}
|
||||||
|
|
5
main.cpp
5
main.cpp
|
@ -44,6 +44,11 @@ int main(int argc, char** argv) {
|
||||||
|
|
||||||
server.Get("/", home_route);
|
server.Get("/", home_route);
|
||||||
server.Get("/style.css", css_route);
|
server.Get("/style.css", css_route);
|
||||||
|
server.Get("/favicon.ico", [](const httplib::Request& req, httplib::Response& res) {
|
||||||
|
res.status = 404;
|
||||||
|
serve_error(req, res, "404: Page not found");
|
||||||
|
});
|
||||||
|
|
||||||
server.Get("/settings", user_settings_route);
|
server.Get("/settings", user_settings_route);
|
||||||
server.Post("/settings", user_settings_route);
|
server.Post("/settings", user_settings_route);
|
||||||
|
|
||||||
|
|
10
models.cpp
10
models.cpp
|
@ -60,6 +60,11 @@ void from_json(const json& j, Account& account) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void from_json(const json& j, Size& size) {
|
||||||
|
j.at("width").get_to(size.width);
|
||||||
|
j.at("height").get_to(size.height);
|
||||||
|
}
|
||||||
|
|
||||||
void from_json(const json& j, Media& media) {
|
void from_json(const json& j, Media& media) {
|
||||||
j.at("type").get_to(media.type);
|
j.at("type").get_to(media.type);
|
||||||
j.at("url").get_to(media.url);
|
j.at("url").get_to(media.url);
|
||||||
|
@ -69,6 +74,11 @@ void from_json(const json& j, Media& media) {
|
||||||
if (!j.at("remote_url").is_null()) {
|
if (!j.at("remote_url").is_null()) {
|
||||||
media.remote_url = j["remote_url"].get<std::string>();
|
media.remote_url = j["remote_url"].get<std::string>();
|
||||||
}
|
}
|
||||||
|
if (media.type == "image" || media.type == "video" || media.type == "gifv") {
|
||||||
|
const json& meta = j.at("meta");
|
||||||
|
media.size = meta.at("original").get<Size>();
|
||||||
|
media.preview_size = meta.at("small").get<Size>();
|
||||||
|
}
|
||||||
if (!j.at("description").is_null()) {
|
if (!j.at("description").is_null()) {
|
||||||
media.description = j["description"].get<std::string>();
|
media.description = j["description"].get<std::string>();
|
||||||
}
|
}
|
||||||
|
|
7
models.h
7
models.h
|
@ -51,11 +51,17 @@ struct Account {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct Size {
|
||||||
|
uint64_t width;
|
||||||
|
uint64_t height;
|
||||||
|
};
|
||||||
struct Media {
|
struct Media {
|
||||||
std::string type;
|
std::string type;
|
||||||
std::string url;
|
std::string url;
|
||||||
std::optional<std::string> preview_url;
|
std::optional<std::string> preview_url;
|
||||||
std::optional<std::string> remote_url;
|
std::optional<std::string> remote_url;
|
||||||
|
std::optional<Size> size;
|
||||||
|
std::optional<Size> preview_size;
|
||||||
std::optional<std::string> description;
|
std::optional<std::string> description;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -108,6 +114,7 @@ struct Instance {
|
||||||
void from_json(const nlohmann::json& j, Emoji& emoji);
|
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, AccountField& field);
|
||||||
void from_json(const nlohmann::json& j, Account& account);
|
void from_json(const nlohmann::json& j, Account& account);
|
||||||
|
void from_json(const nlohmann::json& j, Size& size);
|
||||||
void from_json(const nlohmann::json& j, Media& media);
|
void from_json(const nlohmann::json& j, Media& media);
|
||||||
void from_json(const nlohmann::json& j, PollOption& option);
|
void from_json(const nlohmann::json& j, PollOption& option);
|
||||||
void from_json(const nlohmann::json& j, Poll& poll);
|
void from_json(const nlohmann::json& j, Poll& poll);
|
||||||
|
|
|
@ -5,6 +5,8 @@
|
||||||
#include "../models.h"
|
#include "../models.h"
|
||||||
|
|
||||||
static inline std::string make_title(const Post& post);
|
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) {
|
void status_route(const httplib::Request& req, httplib::Response& res) {
|
||||||
|
@ -48,7 +50,7 @@ void status_route(const httplib::Request& req, httplib::Response& res) {
|
||||||
body.nodes.push_back(serialize_post(req, server, i));
|
body.nodes.push_back(serialize_post(req, server, i));
|
||||||
}
|
}
|
||||||
|
|
||||||
serve(req, res, make_title(*post), std::move(body));
|
serve(req, res, make_title(*post), std::move(body), generate_ogp_nodes(req, *post, server));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -69,3 +71,64 @@ static inline std::string make_title(const Post& post) {
|
||||||
|
|
||||||
return title;
|
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 ? "image" : "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> 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> 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}}, {}));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,8 @@ static inline Element user_header(const httplib::Request& req, const std::string
|
||||||
static inline Element user_link_field(const httplib::Request& req, const Account& account, const AccountField& field);
|
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 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<std::string>& max_id, PostSortingMethod sorting_method);
|
||||||
|
|
||||||
|
|
||||||
void user_route(const httplib::Request& req, httplib::Response& res) {
|
void user_route(const httplib::Request& req, httplib::Response& res) {
|
||||||
using namespace std::string_literals;
|
using namespace std::string_literals;
|
||||||
|
@ -66,7 +68,7 @@ void user_route(const httplib::Request& req, httplib::Response& res) {
|
||||||
body.nodes.push_back(Element("p", {{"class", "more_posts"}}, {"There are no more posts"}));
|
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));
|
serve(req, res, account->display_name + " (@" + account->acct() + ')', std::move(body), generate_ogp_nodes(req, *account, max_id, sorting_method));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -109,7 +111,8 @@ static inline Element user_header(const httplib::Request& req, const std::string
|
||||||
Element("img", {{"class", "user_page-profile"}, {"alt", "User profile picture"}, {"loading", "lazy"}, {"src", account.avatar}}, {}),
|
Element("img", {{"class", "user_page-profile"}, {"alt", "User profile picture"}, {"loading", "lazy"}, {"src", account.avatar}}, {}),
|
||||||
}),
|
}),
|
||||||
Element("span", {
|
Element("span", {
|
||||||
Element("b", {preprocess_html(req, account.emojis, account.display_name)}), account.bot ? " (bot)" : "", " (@", account.acct(), ")", view_on_original,
|
// 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("br"), Element("b", {"Joined: "}), short_time(account.created_at),
|
Element("br"), Element("b", {"Joined: "}), short_time(account.created_at),
|
||||||
Element("br"),
|
Element("br"),
|
||||||
|
@ -160,3 +163,27 @@ static inline Element sorting_method_link(const httplib::Request& req, const std
|
||||||
}
|
}
|
||||||
return a;
|
return a;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline Nodes generate_ogp_nodes(const httplib::Request& req, const Account& account, const std::optional<std::string>& 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;
|
||||||
|
}
|
||||||
|
|
|
@ -18,6 +18,7 @@
|
||||||
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_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 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 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 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);
|
static inline std::vector<lxb_dom_node*> emojify(lxb_dom_document_t* document, std::string str, const std::vector<Emoji>& emojis);
|
||||||
|
|
||||||
|
@ -194,6 +195,35 @@ Element serialize_post(const httplib::Request& req, const std::string& server, c
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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--;
|
||||||
|
}
|
||||||
|
if (out.size() > remove_from) {
|
||||||
|
out.erase(remove_from);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!out.empty()) {
|
||||||
|
size_t remove_to = 0;
|
||||||
|
while (out.size() > remove_to && out[remove_to] == '\n') {
|
||||||
|
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) {
|
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);
|
LXB::HTML::Document document(str.str);
|
||||||
preprocess_html(req, domain_name, emojis, document.body_element());
|
preprocess_html(req, domain_name, emojis, document.body_element());
|
||||||
|
@ -316,11 +346,40 @@ static inline bool should_fix_link(lxb_dom_element_t* element, const std::string
|
||||||
return child == nullptr;
|
return child == nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline lxb_dom_node_t* emojify(lxb_dom_node_t* child, const std::vector<Emoji>& emojis) {
|
static inline void get_text_content(lxb_dom_node_t* node, std::string& out) {
|
||||||
size_t text_content_len;
|
bool is_br = false, is_p = false;
|
||||||
const char* text_content = reinterpret_cast<const char*>(lxb_dom_node_text_content(child, &text_content_len));
|
|
||||||
|
|
||||||
std::vector<lxb_dom_node_t*> nodes = emojify(child->owner_document, std::string(text_content, text_content_len), emojis);
|
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_insert_after(child, nodes[0]);
|
||||||
lxb_dom_node_destroy(child);
|
lxb_dom_node_destroy(child);
|
||||||
|
@ -460,7 +519,7 @@ static inline Element serialize_media(const Media& media) {
|
||||||
video.attributes.push_back({"poster", *media.preview_url});
|
video.attributes.push_back({"poster", *media.preview_url});
|
||||||
}
|
}
|
||||||
return video;
|
return video;
|
||||||
} else if (media.type == "unknown" && media.remote_url) {
|
} else if (media.type == "unknown") {
|
||||||
if (media.remote_url) {
|
if (media.remote_url) {
|
||||||
// https://botsin.space/@lina@vt.social/111053598696451525
|
// https://botsin.space/@lina@vt.social/111053598696451525
|
||||||
return Element("a", {{"href", *media.remote_url}}, {"Media is not available from this instance, view externally"});
|
return Element("a", {{"href", *media.remote_url}}, {"Media is not available from this instance, view externally"});
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
#include <httplib/httplib.h>
|
#include <httplib/httplib.h>
|
||||||
#include "blankie/serializer.h"
|
#include "blankie/serializer.h"
|
||||||
|
#include "lxb_wrapper.h"
|
||||||
struct Post; // forward declaration from models.h
|
struct Post; // forward declaration from models.h
|
||||||
struct Emoji; // forward declaration from models.h
|
struct Emoji; // forward declaration from models.h
|
||||||
class CurlUrl; // forward declaration from curlu_wrapper.h
|
class CurlUrl; // forward declaration from curlu_wrapper.h
|
||||||
|
@ -24,5 +25,7 @@ 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);
|
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::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);
|
blankie::html::HTMLString preprocess_html(const httplib::Request& req, const std::vector<Emoji>& emojis, const std::string& str);
|
||||||
|
|
Loading…
Reference in New Issue