Compare commits
4 Commits
9acae28193
...
9b21060b3a
Author | SHA1 | Date |
---|---|---|
blankie | 9b21060b3a | |
blankie | 973a0eada2 | |
blankie | d1711a040b | |
blankie | 9b6f6a4bcc |
|
@ -5,10 +5,14 @@ project(coyote C CXX)
|
|||
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
find_package(CURL REQUIRED)
|
||||
find_package(OpenSSL REQUIRED)
|
||||
|
||||
set(HTTPLIB_REQUIRE_OPENSSL ON)
|
||||
add_subdirectory(thirdparty/httplib)
|
||||
|
||||
set(LEXBOR_BUILD_SHARED OFF)
|
||||
add_subdirectory(thirdparty/lexbor)
|
||||
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(HIREDIS REQUIRED hiredis)
|
||||
|
||||
|
@ -29,7 +33,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 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 openssl_wrapper.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}
|
||||
|
@ -39,6 +43,6 @@ set_target_properties(${PROJECT_NAME}
|
|||
CXX_EXTENSIONS NO
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE thirdparty ${HIREDIS_INCLUDE_DIRS})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json CURL::libcurl httplib::httplib lexbor_static ${HIREDIS_LINK_LIBRARIES})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json CURL::libcurl OpenSSL::Crypto httplib::httplib lexbor_static ${HIREDIS_LINK_LIBRARIES})
|
||||
target_compile_definitions(${PROJECT_NAME} PRIVATE ${DEFINITIONS})
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${FLAGS})
|
||||
|
|
|
@ -15,6 +15,7 @@ Copy `example_config.json` to a file with any name you like
|
|||
liking. Here's a list of what they are:
|
||||
- `bind_host` (string): What address to bind to
|
||||
- `bind_port` (zero or positive integer): What port to bind to
|
||||
- `hmac_key` (hex string): A secret key to be used; generate with `head -c32 /dev/urandom | basenc --base16`
|
||||
- `canonical_origin` (string or null): A fallback canonical origin if set, useful if you're, say, running Coyote behind Ngrok
|
||||
- `redis` (object)
|
||||
- `enabled` (boolean)
|
||||
|
|
51
client.cpp
51
client.cpp
|
@ -2,6 +2,7 @@
|
|||
#include <stdexcept>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "hex.h"
|
||||
#include "client.h"
|
||||
#include "models.h"
|
||||
#include "curlu_wrapper.h"
|
||||
|
@ -11,8 +12,6 @@ MastodonClient mastodon_client;
|
|||
|
||||
static void lowercase(std::string& str);
|
||||
static void handle_post_server(Post& post, const std::string& host);
|
||||
static std::string url_encode(const std::string& in);
|
||||
static inline void hexencode(char c, char out[2]);
|
||||
|
||||
static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp);
|
||||
static void share_unlock(CURL* curl, curl_lock_data data, void* clientp);
|
||||
|
@ -90,7 +89,7 @@ std::optional<Account> MastodonClient::get_account_by_username(std::string host,
|
|||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/accounts/lookup");
|
||||
url.set(CURLUPART_QUERY, "acct="s + url_encode(username));
|
||||
url.set(CURLUPART_QUERY, "acct="s + percent_encode(username));
|
||||
try {
|
||||
Account account = this->_send_request("coyote:"s + host + ":@" + username, url);
|
||||
account.same_server = host == account.server;
|
||||
|
@ -111,7 +110,7 @@ std::vector<Post> MastodonClient::get_pinned_posts(std::string host, const std::
|
|||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/accounts/"s + url_encode(account_id) + "/statuses");
|
||||
url.set(CURLUPART_PATH, "/api/v1/accounts/"s + percent_encode(account_id) + "/statuses");
|
||||
url.set(CURLUPART_QUERY, "pinned=true");
|
||||
std::vector<Post> posts = this->_send_request("coyote:"s + host + ':' + account_id + ":pinned", url);
|
||||
|
||||
|
@ -129,7 +128,7 @@ std::vector<Post> MastodonClient::get_posts(const std::string& host, const std::
|
|||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/accounts/"s + url_encode(account_id) + "/statuses");
|
||||
url.set(CURLUPART_PATH, "/api/v1/accounts/"s + percent_encode(account_id) + "/statuses");
|
||||
url.set(CURLUPART_QUERY, sorting_parameters[sorting_method]);
|
||||
if (max_id) {
|
||||
url.set(CURLUPART_QUERY, "max_id="s + std::move(*max_id), CURLU_URLENCODE | CURLU_APPENDQUERY);
|
||||
|
@ -150,7 +149,7 @@ std::optional<Post> MastodonClient::get_post(const std::string& host, std::strin
|
|||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/statuses/"s + url_encode(std::move(id)));
|
||||
url.set(CURLUPART_PATH, "/api/v1/statuses/"s + percent_encode(std::move(id)));
|
||||
try {
|
||||
Post post = this->_send_request(std::nullopt, url);
|
||||
handle_post_server(post, host);
|
||||
|
@ -170,7 +169,7 @@ PostContext MastodonClient::get_post_context(const std::string& host, std::strin
|
|||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/statuses/"s + url_encode(std::move(id)) + "/context");
|
||||
url.set(CURLUPART_PATH, "/api/v1/statuses/"s + percent_encode(std::move(id)) + "/context");
|
||||
PostContext context = this->_send_request(std::nullopt, url);
|
||||
|
||||
for (Post& post : context.ancestors) {
|
||||
|
@ -189,7 +188,7 @@ std::vector<Post> MastodonClient::get_tag_timeline(const std::string& host, cons
|
|||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/timelines/tag/"s + url_encode(tag));
|
||||
url.set(CURLUPART_PATH, "/api/v1/timelines/tag/"s + percent_encode(tag));
|
||||
if (max_id) {
|
||||
url.set(CURLUPART_QUERY, "max_id="s + std::move(*max_id), CURLU_URLENCODE | CURLU_APPENDQUERY);
|
||||
}
|
||||
|
@ -313,42 +312,6 @@ static void handle_post_server(Post& post, const std::string& host) {
|
|||
}
|
||||
}
|
||||
|
||||
static std::string url_encode(const std::string& in) {
|
||||
std::string out;
|
||||
char encoded[2];
|
||||
size_t pos = 0;
|
||||
size_t last_pos = 0;
|
||||
|
||||
out.reserve(in.size());
|
||||
while ((pos = in.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", pos)) != std::string::npos) {
|
||||
out.append(in, last_pos, pos - last_pos);
|
||||
hexencode(in[pos], encoded);
|
||||
out += '%';
|
||||
out.append(encoded, 2);
|
||||
pos++;
|
||||
last_pos = pos;
|
||||
}
|
||||
|
||||
if (in.size() > last_pos) {
|
||||
out.append(in, last_pos);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
static inline void hexencode(char c, char out[2]) {
|
||||
char nibble1 = (c >> 4) & 0xF;
|
||||
char nibble2 = c & 0xF;
|
||||
|
||||
auto hexencode = [](char nibble) {
|
||||
return static_cast<char>(nibble < 10
|
||||
? '0' + nibble
|
||||
: 'A' + nibble - 10);
|
||||
};
|
||||
out[0] = hexencode(nibble1);
|
||||
out[1] = hexencode(nibble2);
|
||||
}
|
||||
|
||||
static void share_lock(CURL* curl, curl_lock_data data, curl_lock_access access, void* clientp) {
|
||||
(void)curl;
|
||||
(void)access;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
#include <stdexcept>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "hex.h"
|
||||
#include "file.h"
|
||||
#include "config.h"
|
||||
|
||||
|
@ -47,6 +48,7 @@ void from_json(const nlohmann::json& j, Config& conf) {
|
|||
throw std::invalid_argument("Invalid port to bind to: "s + std::to_string(conf.bind_port));
|
||||
}
|
||||
|
||||
conf.hmac_key = hex_decode(j.at("hmac_key").get_ref<const std::string&>());
|
||||
if (j.at("canonical_origin").is_string()) {
|
||||
conf.canonical_origin = j["canonical_origin"].get<std::string>();
|
||||
}
|
||||
|
|
2
config.h
2
config.h
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <variant>
|
||||
#include <optional>
|
||||
#include <nlohmann/json_fwd.hpp>
|
||||
|
@ -22,6 +23,7 @@ struct RedisConfig {
|
|||
struct Config {
|
||||
std::string bind_host = "127.0.0.1";
|
||||
int bind_port = 8080;
|
||||
std::vector<char> hmac_key;
|
||||
std::optional<std::string> canonical_origin;
|
||||
std::optional<RedisConfig> redis_config;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
{
|
||||
"bind_host": "127.0.0.1",
|
||||
"bind_port": 8080,
|
||||
"hmac_key": "AA",
|
||||
"canonical_origin": null,
|
||||
"redis": {
|
||||
"enabled": true,
|
||||
|
|
|
@ -0,0 +1,82 @@
|
|||
#include <stdexcept>
|
||||
|
||||
#include "hex.h"
|
||||
|
||||
static void hex_encode(char c, char out[2]);
|
||||
static inline char hex_decode(char nibble1, char nibble2);
|
||||
|
||||
|
||||
std::string hex_encode(const char* in, size_t in_size) {
|
||||
std::string out;
|
||||
|
||||
out.reserve(in_size * 2);
|
||||
for (size_t i = 0; i < in_size; i++) {
|
||||
char encoded[2];
|
||||
hex_encode(in[i], encoded);
|
||||
out.append(encoded, 2);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::string percent_encode(std::string_view in) {
|
||||
std::string out;
|
||||
char encoded[2];
|
||||
size_t pos = 0;
|
||||
size_t last_pos = 0;
|
||||
|
||||
out.reserve(in.size());
|
||||
while ((pos = in.find_first_not_of("abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789", pos)) != std::string::npos) {
|
||||
out.append(in, last_pos, pos - last_pos);
|
||||
hex_encode(in[pos], encoded);
|
||||
out += '%';
|
||||
out.append(encoded, 2);
|
||||
pos++;
|
||||
last_pos = pos;
|
||||
}
|
||||
|
||||
if (in.size() > last_pos) {
|
||||
out.append(in, last_pos);
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
std::vector<char> hex_decode(std::string_view in) {
|
||||
if (in.size() % 2 != 0) {
|
||||
throw std::invalid_argument("hex_decode(): hex string with an odd size passed");
|
||||
}
|
||||
std::vector<char> out;
|
||||
|
||||
out.reserve(in.size() / 2);
|
||||
for (size_t i = 0; i < in.size(); i += 2) {
|
||||
out.push_back(hex_decode(in[i], in[i + 1]));
|
||||
}
|
||||
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
static void hex_encode(char c, char out[2]) {
|
||||
char nibble1 = (c >> 4) & 0xF;
|
||||
char nibble2 = c & 0xF;
|
||||
|
||||
auto hex_encode = [](char nibble) {
|
||||
return static_cast<char>(nibble < 10
|
||||
? '0' + nibble
|
||||
: 'A' + nibble - 10);
|
||||
};
|
||||
out[0] = hex_encode(nibble1);
|
||||
out[1] = hex_encode(nibble2);
|
||||
}
|
||||
|
||||
static inline char hex_decode(char nibble1, char nibble2) {
|
||||
auto hex_decode = [](char nibble) {
|
||||
if (nibble >= '0' && nibble <= '9') return nibble - '0';
|
||||
if (nibble >= 'a' && nibble <= 'f') return nibble - 'a' + 10;
|
||||
if (nibble >= 'A' && nibble <= 'F') return nibble - 'A' + 10;
|
||||
throw std::invalid_argument("hex_decode(): invalid nibble");
|
||||
};
|
||||
|
||||
return static_cast<char>((hex_decode(nibble1) << 4) | hex_decode(nibble2));
|
||||
}
|
|
@ -0,0 +1,12 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
std::string hex_encode(const char* in, size_t in_size);
|
||||
inline std::string hex_encode(const std::vector<char>& in) {
|
||||
return hex_encode(in.data(), in.size());
|
||||
}
|
||||
std::string percent_encode(std::string_view in);
|
||||
|
||||
std::vector<char> hex_decode(std::string_view in);
|
|
@ -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"
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
#include <openssl/hmac.h>
|
||||
#include <openssl/rand.h>
|
||||
#include "openssl_wrapper.h"
|
||||
|
||||
std::vector<char> secure_random_bytes(int num) {
|
||||
if (num < 0) {
|
||||
throw std::invalid_argument("secure_random_bytes(): num variable out of range (num < 0)");
|
||||
}
|
||||
|
||||
std::vector<char> bytes(static_cast<size_t>(num), 0);
|
||||
if (RAND_bytes(reinterpret_cast<unsigned char*>(bytes.data()), num) == 1) {
|
||||
return bytes;
|
||||
} else {
|
||||
throw OpenSSLException(ERR_get_error());
|
||||
}
|
||||
}
|
||||
|
||||
std::array<char, 32> hmac_sha3_256(const std::vector<char>& key, const std::vector<char>& data) {
|
||||
char hmac[32];
|
||||
unsigned int md_len;
|
||||
|
||||
std::unique_ptr<EVP_MD, decltype(&EVP_MD_free)> md(EVP_MD_fetch(nullptr, "SHA3-256", nullptr), EVP_MD_free);
|
||||
if (HMAC(md.get(), key.data(), static_cast<int>(key.size()), reinterpret_cast<const unsigned char*>(data.data()), data.size(), reinterpret_cast<unsigned char*>(hmac), &md_len)) {
|
||||
if (md_len != 32) {
|
||||
throw std::runtime_error("hmac_sha3_256(): HMAC() returned an unexpected size");
|
||||
}
|
||||
|
||||
return std::to_array(hmac);
|
||||
} else {
|
||||
throw OpenSSLException(ERR_get_error());
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
#pragma once
|
||||
|
||||
#include <array>
|
||||
#include <vector>
|
||||
#include <exception>
|
||||
|
||||
#include <openssl/err.h>
|
||||
|
||||
class OpenSSLException : public std::exception {
|
||||
public:
|
||||
OpenSSLException(unsigned long e) {
|
||||
ERR_error_string_n(e, this->_str, 1024);
|
||||
}
|
||||
|
||||
const char* what() const noexcept {
|
||||
return this->_str;
|
||||
}
|
||||
|
||||
private:
|
||||
char _str[1024];
|
||||
};
|
||||
|
||||
std::vector<char> secure_random_bytes(int num);
|
||||
std::array<char, 32> hmac_sha3_256(const std::vector<char>& key, const std::vector<char>& data);
|
|
@ -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"
|
||||
|
|
|
@ -1,23 +1,58 @@
|
|||
#include "routes.h"
|
||||
#include "../hex.h"
|
||||
#include "../config.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../settings.h"
|
||||
#include "../timeutils.h"
|
||||
#include "../curlu_wrapper.h"
|
||||
#include "../openssl_wrapper.h"
|
||||
|
||||
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value);
|
||||
static inline std::string generate_csrf_token(void);
|
||||
static inline bool validate_csrf_token(const httplib::Request& req, httplib::Response& res, std::string_view csrf_token, std::string_view query_csrf_token);
|
||||
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value, bool session = false);
|
||||
static bool safe_memcmp(const char* s1, const char* s2, size_t n);
|
||||
|
||||
|
||||
void user_settings_route(const httplib::Request& req, httplib::Response& res) {
|
||||
UserSettings settings;
|
||||
Cookies cookies = parse_cookies(req);
|
||||
std::string csrf_token;
|
||||
|
||||
if (req.method == "POST") {
|
||||
if (!cookies.contains("csrf-token")) {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "Missing CSRF token cookie, are cookies enabled?");
|
||||
return;
|
||||
}
|
||||
csrf_token = cookies["csrf-token"];
|
||||
|
||||
auto query_csrf_token = req.params.find("csrf-token");
|
||||
if (query_csrf_token == req.params.end()) {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "Missing CSRF token query parameter");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!validate_csrf_token(req, res, csrf_token, query_csrf_token->second)) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const auto& i : req.params) {
|
||||
settings.set(i.first, i.second);
|
||||
}
|
||||
|
||||
set_cookie(req, res, "auto-open-cw", settings.auto_open_cw ? "true" : "false");
|
||||
} else {
|
||||
settings.load_from_cookies(req);
|
||||
for (auto &[name, value] : cookies) {
|
||||
settings.set(name, value);
|
||||
}
|
||||
|
||||
if (cookies.contains("csrf-token")) {
|
||||
csrf_token = cookies["csrf-token"];
|
||||
} else {
|
||||
csrf_token = generate_csrf_token();
|
||||
set_cookie(req, res, "csrf-token", csrf_token, true);
|
||||
}
|
||||
}
|
||||
|
||||
Element auto_open_cw_checkbox("input", {{"type", "checkbox"}, {"name", "auto-open-cw"}, {"value", "true"}}, {});
|
||||
|
@ -33,6 +68,7 @@ void user_settings_route(const httplib::Request& req, httplib::Response& res) {
|
|||
}),
|
||||
|
||||
Element("br"),
|
||||
Element("input", {{"type", "hidden"}, {"name", "csrf-token"}, {"value", csrf_token}}, {}),
|
||||
Element("input", {{"type", "submit"}, {"value", "Save"}}, {}),
|
||||
}),
|
||||
Element("form", {{"class", "user_settings_page-form"}, {"method", "get"}, {"action", get_origin(req)}}, {
|
||||
|
@ -49,16 +85,69 @@ void user_settings_route(const httplib::Request& req, httplib::Response& res) {
|
|||
}
|
||||
|
||||
|
||||
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value) {
|
||||
static inline std::string generate_csrf_token(void) {
|
||||
std::vector<char> raw_token = secure_random_bytes(32);
|
||||
std::array<char, 32> raw_token_hmac = hmac_sha3_256(config.hmac_key, raw_token);
|
||||
|
||||
return hex_encode(raw_token) + '.' + hex_encode(raw_token_hmac.data(), raw_token_hmac.size());
|
||||
}
|
||||
|
||||
static inline bool validate_csrf_token(const httplib::Request& req, httplib::Response& res, std::string_view csrf_token, std::string_view query_csrf_token) {
|
||||
if (csrf_token.size() != query_csrf_token.size() || !safe_memcmp(csrf_token.data(), query_csrf_token.data(), csrf_token.size())) {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "CSRF token cookie and CSRF token query parameter do not match");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (csrf_token.size() != 64 + 1 + 64 || csrf_token[64] != '.') {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "CSRF token is in an unknown format");
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<char> raw_token, raw_token_hmac;
|
||||
try {
|
||||
raw_token = hex_decode(csrf_token.substr(0, 64));
|
||||
raw_token_hmac = hex_decode(csrf_token.substr(64 + 1, 64));
|
||||
} catch (const std::exception& e) {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "Failed to parse CSRF token", e.what());
|
||||
return false;
|
||||
}
|
||||
|
||||
std::array<char, 32> our_raw_token_hmac = hmac_sha3_256(config.hmac_key, raw_token);
|
||||
if (!safe_memcmp(raw_token_hmac.data(), our_raw_token_hmac.data(), 32)) {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "CSRF token HMAC is not correct");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value, bool session) {
|
||||
CurlUrl origin;
|
||||
origin.set(CURLUPART_URL, get_origin(req));
|
||||
|
||||
std::string header = std::string(key) + '=' + std::string(value)
|
||||
+ "; HttpOnly; SameSite=Strict; Domain=" + origin.get(CURLUPART_HOST).get() + "; Path=" + origin.get(CURLUPART_PATH).get()
|
||||
+ "; Expires=" + to_web_date(current_time() + 365 * 24 * 60 * 60);
|
||||
+ "; HttpOnly; SameSite=Lax; Domain=" + origin.get(CURLUPART_HOST).get() + "; Path=" + origin.get(CURLUPART_PATH).get();
|
||||
if (!session) {
|
||||
header += "; Expires=";
|
||||
header += to_web_date(current_time() + 365 * 24 * 60 * 60);
|
||||
}
|
||||
if (strcmp(origin.get(CURLUPART_SCHEME).get(), "https") == 0) {
|
||||
header += "; Secure";
|
||||
}
|
||||
|
||||
res.set_header("Set-Cookie", header);
|
||||
}
|
||||
|
||||
static bool safe_memcmp(const char* s1, const char* s2, size_t n) {
|
||||
bool equal = true;
|
||||
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
equal &= s1[i] == s2[i];
|
||||
}
|
||||
|
||||
return equal;
|
||||
}
|
||||
|
|
492
servehelper.cpp
492
servehelper.cpp
|
@ -4,31 +4,14 @@
|
|||
#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);
|
||||
static inline void parse_cookies(std::string_view str, Cookies& cookies);
|
||||
static inline bool lowercase_compare(std::string_view lhs, std::string_view rhs);
|
||||
|
||||
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) {
|
||||
|
@ -169,434 +152,59 @@ bool should_send_304(const httplib::Request& req, uint64_t 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) {
|
||||
|
||||
Cookies parse_cookies(const httplib::Request& req) {
|
||||
Cookies cookies;
|
||||
|
||||
for (const auto& i : req.headers) {
|
||||
if (lowercase_compare(i.first, "cookie")) {
|
||||
parse_cookies(i.second, cookies);
|
||||
}
|
||||
}
|
||||
|
||||
return cookies;
|
||||
}
|
||||
|
||||
|
||||
|
||||
static inline void parse_cookies(std::string_view str, Cookies& cookies) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
if (post.reblog) {
|
||||
PostStatus post_status = {
|
||||
fa_retweet,
|
||||
preprocess_html(req, post.account.emojis, post.account.display_name + " boosted"),
|
||||
size_t offset = 0;
|
||||
size_t new_offset = 0;
|
||||
const char* delimiter = "; ";
|
||||
size_t delimiter_len = strlen(delimiter);
|
||||
|
||||
while (offset < str.size()) {
|
||||
new_offset = str.find(delimiter, offset);
|
||||
|
||||
std::string_view item = str.substr(offset, new_offset != std::string_view::npos ? new_offset - offset : std::string_view::npos);
|
||||
size_t equal_offset = item.find('=');
|
||||
if (equal_offset == std::string_view::npos) {
|
||||
throw std::invalid_argument("invalid user setting item: "s + std::string(item));
|
||||
}
|
||||
cookies.insert({std::string(item.substr(0, equal_offset)), std::string(item.substr(equal_offset + 1))});
|
||||
|
||||
if (new_offset == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
offset = new_offset + delimiter_len;
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool lowercase_compare(std::string_view lhs, std::string_view rhs) {
|
||||
if (lhs.size() != rhs.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lower = [](char c) {
|
||||
return c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c;
|
||||
};
|
||||
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);
|
||||
for (size_t i = 0; i < lhs.size(); i++) {
|
||||
if (lower(lhs[i]) != lower(rhs[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <unordered_map>
|
||||
|
||||
#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;
|
||||
using Node = blankie::html::Node;
|
||||
using Nodes = std::vector<Node>;
|
||||
using Cookies = std::unordered_map<std::string, std::string>;
|
||||
|
||||
void serve(const httplib::Request& req, httplib::Response& res, std::string title, Element element, Nodes extra_head = {});
|
||||
void serve_error(const httplib::Request& req, httplib::Response& res,
|
||||
|
@ -23,9 +22,4 @@ 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);
|
||||
Cookies parse_cookies(const httplib::Request& req);
|
||||
|
|
51
settings.cpp
51
settings.cpp
|
@ -1,9 +1,8 @@
|
|||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include "settings.h"
|
||||
#include "servehelper.h"
|
||||
|
||||
static void set_settings(std::string_view str, const char* delimiter, UserSettings& settings);
|
||||
static inline bool lowercase_compare(std::string_view lhs, std::string_view rhs);
|
||||
static bool parse_bool(std::string_view value);
|
||||
|
||||
|
||||
|
@ -14,54 +13,14 @@ void UserSettings::set(std::string_view key, std::string_view value) {
|
|||
}
|
||||
|
||||
void UserSettings::load_from_cookies(const httplib::Request& req) {
|
||||
for (const auto& i : req.headers) {
|
||||
if (lowercase_compare(i.first, "cookie")) {
|
||||
set_settings(i.second, "; ", *this);
|
||||
}
|
||||
Cookies cookies = parse_cookies(req);
|
||||
|
||||
for (auto &[name, value] : cookies) {
|
||||
this->set(name, value);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
static void set_settings(std::string_view str, const char* delimiter, UserSettings& settings) {
|
||||
using namespace std::string_literals;
|
||||
size_t offset = 0;
|
||||
size_t new_offset = 0;
|
||||
size_t delimiter_len = strlen(delimiter);
|
||||
|
||||
while (offset < str.size()) {
|
||||
new_offset = str.find(delimiter, offset);
|
||||
|
||||
std::string_view item = str.substr(offset, new_offset != std::string_view::npos ? new_offset - offset : std::string_view::npos);
|
||||
size_t equal_offset = item.find('=');
|
||||
if (equal_offset == std::string_view::npos) {
|
||||
throw std::invalid_argument("invalid user setting item: "s + std::string(item));
|
||||
}
|
||||
settings.set(item.substr(0, equal_offset), item.substr(equal_offset + 1));
|
||||
|
||||
if (new_offset == std::string_view::npos) {
|
||||
break;
|
||||
}
|
||||
offset = new_offset + delimiter_len;
|
||||
}
|
||||
}
|
||||
|
||||
static inline bool lowercase_compare(std::string_view lhs, std::string_view rhs) {
|
||||
if (lhs.size() != rhs.size()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
auto lower = [](char c) {
|
||||
return c >= 'A' && c <= 'Z' ? c - 'A' + 'a' : c;
|
||||
};
|
||||
for (size_t i = 0; i < lhs.size(); i++) {
|
||||
if (lower(lhs[i]) != lower(rhs[i])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
static bool parse_bool(std::string_view value) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
|
|
Loading…
Reference in New Issue