Compare commits
6 Commits
5d74e1124d
...
402e74dd6e
Author | SHA1 | Date |
---|---|---|
blankie | 402e74dd6e | |
blankie | e231afb49c | |
blankie | ab8f5569be | |
blankie | a21c1e22bc | |
blankie | 57202de5e8 | |
blankie | 6130e92114 |
|
@ -29,8 +29,8 @@ 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 models.cpp client.cpp servehelper.cpp timeutils.cpp hiredis_wrapper.cpp
|
||||
routes/home.cpp routes/css.cpp routes/user.cpp routes/status.cpp routes/tags.cpp routes/about.cpp
|
||||
add_executable(${PROJECT_NAME} main.cpp numberhelper.cpp config.cpp settings.cpp models.cpp client.cpp servehelper.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}
|
||||
PROPERTIES
|
||||
|
|
75
client.cpp
75
client.cpp
|
@ -4,6 +4,7 @@
|
|||
|
||||
#include "client.h"
|
||||
#include "models.h"
|
||||
#include "curlu_wrapper.h"
|
||||
#include "hiredis_wrapper.h"
|
||||
|
||||
MastodonClient mastodon_client;
|
||||
|
@ -85,8 +86,13 @@ std::optional<Account> MastodonClient::get_account_by_username(std::string host,
|
|||
username.erase(username.size() - host.size() - 1);
|
||||
}
|
||||
|
||||
CurlUrl url;
|
||||
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));
|
||||
try {
|
||||
Account account = this->_send_request("coyote:"s + host + ":@" + username, "https://"s + host + "/api/v1/accounts/lookup?acct=" + url_encode(username));
|
||||
Account account = this->_send_request("coyote:"s + host + ":@" + username, url);
|
||||
account.same_server = host == account.server;
|
||||
return account;
|
||||
} catch (const MastodonException& e) {
|
||||
|
@ -102,7 +108,12 @@ std::vector<Post> MastodonClient::get_pinned_posts(std::string host, const std::
|
|||
using namespace std::string_literals;
|
||||
lowercase(host);
|
||||
|
||||
std::vector<Post> posts = this->_send_request("coyote:"s + host + ':' + account_id + ":pinned", "https://"s + host + "/api/v1/accounts/" + account_id + "/statuses?pinned=true");
|
||||
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_QUERY, "pinned=true");
|
||||
std::vector<Post> posts = this->_send_request("coyote:"s + host + ':' + account_id + ":pinned", url);
|
||||
|
||||
for (Post& post : posts) {
|
||||
handle_post_server(post, host);
|
||||
|
@ -113,22 +124,17 @@ std::vector<Post> MastodonClient::get_pinned_posts(std::string host, const std::
|
|||
|
||||
std::vector<Post> MastodonClient::get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
const char* sorting_parameters[3] = {"exclude_replies=true", "", "only_media=true"};
|
||||
std::string query = sorting_parameters[sorting_method];
|
||||
|
||||
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_QUERY, sorting_parameters[sorting_method]);
|
||||
if (max_id) {
|
||||
if (!query.empty()) {
|
||||
query += '&';
|
||||
}
|
||||
query += "max_id=";
|
||||
query += std::move(*max_id);
|
||||
url.set(CURLUPART_QUERY, "max_id="s + std::move(*max_id), CURLU_URLENCODE | CURLU_APPENDQUERY);
|
||||
}
|
||||
|
||||
std::string url = "https://"s + host + "/api/v1/accounts/" + account_id + "/statuses";
|
||||
if (!query.empty()) {
|
||||
url += '?';
|
||||
url += query;
|
||||
}
|
||||
std::vector<Post> posts = this->_send_request(std::nullopt, url);
|
||||
|
||||
for (Post& post : posts) {
|
||||
|
@ -138,11 +144,15 @@ std::vector<Post> MastodonClient::get_posts(const std::string& host, const std::
|
|||
return posts;
|
||||
}
|
||||
|
||||
std::optional<Post> MastodonClient::get_post(const std::string& host, const std::string& id) {
|
||||
std::optional<Post> MastodonClient::get_post(const std::string& host, std::string id) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
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)));
|
||||
try {
|
||||
Post post = this->_send_request(std::nullopt, "https://"s + host + "/api/v1/statuses/" + id);
|
||||
Post post = this->_send_request(std::nullopt, url);
|
||||
handle_post_server(post, host);
|
||||
return post;
|
||||
} catch (const MastodonException& e) {
|
||||
|
@ -154,10 +164,14 @@ std::optional<Post> MastodonClient::get_post(const std::string& host, const std:
|
|||
}
|
||||
}
|
||||
|
||||
PostContext MastodonClient::get_post_context(const std::string& host, const std::string& id) {
|
||||
PostContext MastodonClient::get_post_context(const std::string& host, std::string id) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
PostContext context = this->_send_request(std::nullopt, "https://"s + host + "/api/v1/statuses/" + id + "/context");
|
||||
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");
|
||||
PostContext context = this->_send_request(std::nullopt, url);
|
||||
|
||||
for (Post& post : context.ancestors) {
|
||||
handle_post_server(post, host);
|
||||
|
@ -172,10 +186,12 @@ PostContext MastodonClient::get_post_context(const std::string& host, const std:
|
|||
std::vector<Post> MastodonClient::get_tag_timeline(const std::string& host, const std::string& tag, std::optional<std::string> max_id) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
std::string url = "https://"s + host + "/api/v1/timelines/tag/" + url_encode(tag);
|
||||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/timelines/tag/"s + url_encode(tag));
|
||||
if (max_id) {
|
||||
url += "?max_id=";
|
||||
url += std::move(*max_id);
|
||||
url.set(CURLUPART_QUERY, "max_id="s + std::move(*max_id), CURLU_URLENCODE | CURLU_APPENDQUERY);
|
||||
}
|
||||
|
||||
std::vector<Post> posts = this->_send_request(std::nullopt, url);
|
||||
|
@ -189,7 +205,11 @@ Instance MastodonClient::get_instance(std::string host) {
|
|||
using namespace std::string_literals;
|
||||
lowercase(host);
|
||||
|
||||
Instance instance = this->_send_request("coyote:"s + host + ":instance", "https://"s + host + "/api/v2/instance");
|
||||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v2/instance");
|
||||
Instance instance = this->_send_request("coyote:"s + host + ":instance", url);
|
||||
instance.contact_account.same_server = instance.contact_account.server == host;
|
||||
return instance;
|
||||
}
|
||||
|
@ -198,7 +218,11 @@ blankie::html::HTMLString MastodonClient::get_extended_description(std::string h
|
|||
using namespace std::string_literals;
|
||||
lowercase(host);
|
||||
|
||||
nlohmann::json j = this->_send_request("coyote:"s + host + ":desc", "https://"s + host + "/api/v1/instance/extended_description");
|
||||
CurlUrl url;
|
||||
url.set(CURLUPART_SCHEME, "https");
|
||||
url.set(CURLUPART_HOST, host);
|
||||
url.set(CURLUPART_PATH, "/api/v1/instance/extended_description");
|
||||
nlohmann::json j = this->_send_request("coyote:"s + host + ":desc", url);
|
||||
return blankie::html::HTMLString(j.at("content").get<std::string>());
|
||||
}
|
||||
|
||||
|
@ -234,7 +258,7 @@ CURL* MastodonClient::_get_easy() {
|
|||
return curl;
|
||||
}
|
||||
|
||||
nlohmann::json MastodonClient::_send_request(std::optional<std::string> cache_key, const std::string& url) {
|
||||
nlohmann::json MastodonClient::_send_request(std::optional<std::string> cache_key, const CurlUrl& url) {
|
||||
std::optional<std::string> cached;
|
||||
if (redis && cache_key && (cached = redis->get(*cache_key))) {
|
||||
return nlohmann::json::parse(std::move(*cached));
|
||||
|
@ -243,10 +267,11 @@ nlohmann::json MastodonClient::_send_request(std::optional<std::string> cache_ke
|
|||
std::string res;
|
||||
CURL* curl = this->_get_easy();
|
||||
|
||||
setopt(curl, CURLOPT_URL, url.c_str());
|
||||
setopt(curl, CURLOPT_CURLU, url.get());
|
||||
setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_cb);
|
||||
setopt(curl, CURLOPT_WRITEDATA, &res);
|
||||
CURLcode code = curl_easy_perform(curl);
|
||||
setopt(curl, CURLOPT_CURLU, nullptr);
|
||||
if (code) {
|
||||
throw CurlException(code);
|
||||
}
|
||||
|
|
7
client.h
7
client.h
|
@ -8,6 +8,7 @@
|
|||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "models.h"
|
||||
class CurlUrl; // forward declaration from curlu_wrapper.h
|
||||
|
||||
class CurlException : public std::exception {
|
||||
public:
|
||||
|
@ -68,8 +69,8 @@ public:
|
|||
std::vector<Post> get_pinned_posts(std::string host, const std::string& account_id);
|
||||
std::vector<Post> get_posts(const std::string& host, const std::string& account_id, PostSortingMethod sorting_method, std::optional<std::string> max_id);
|
||||
|
||||
std::optional<Post> get_post(const std::string& host, const std::string& id);
|
||||
PostContext get_post_context(const std::string& host, const std::string& id);
|
||||
std::optional<Post> get_post(const std::string& host, std::string id);
|
||||
PostContext get_post_context(const std::string& host, std::string id);
|
||||
|
||||
std::vector<Post> get_tag_timeline(const std::string& host, const std::string& tag, std::optional<std::string> max_id);
|
||||
|
||||
|
@ -78,7 +79,7 @@ public:
|
|||
|
||||
private:
|
||||
CURL* _get_easy();
|
||||
nlohmann::json _send_request(std::optional<std::string> cache_key, const std::string& url);
|
||||
nlohmann::json _send_request(std::optional<std::string> cache_key, const CurlUrl& url);
|
||||
long _response_status_code();
|
||||
|
||||
std::mutex _share_locks[CURL_LOCK_DATA_LAST];
|
||||
|
|
|
@ -0,0 +1,77 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <exception>
|
||||
#include <stdexcept>
|
||||
#include <curl/curl.h>
|
||||
|
||||
class CurlUrlException : public std::exception {
|
||||
public:
|
||||
CurlUrlException(CURLUcode code_) : code(code_) {
|
||||
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
|
||||
snprintf(this->_id_buf, 64, "curl url error %d", this->code);
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* what() const noexcept {
|
||||
#if CURL_AT_LEAST_VERSION(7, 80, 0)
|
||||
return curl_url_strerror(this->code);
|
||||
#else
|
||||
return this->_id_buf;
|
||||
#endif
|
||||
}
|
||||
|
||||
CURLUcode code;
|
||||
|
||||
private:
|
||||
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
|
||||
char _id_buf[64];
|
||||
#endif
|
||||
};
|
||||
|
||||
using CurlStr = std::unique_ptr<char, decltype(&curl_free)>;
|
||||
class CurlUrl {
|
||||
public:
|
||||
CurlUrl(const CurlUrl&) = delete;
|
||||
CurlUrl& operator=(const CurlUrl&) = delete;
|
||||
|
||||
CurlUrl() {
|
||||
this->_ptr = curl_url();
|
||||
if (!this->_ptr) {
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
}
|
||||
~CurlUrl() {
|
||||
curl_url_cleanup(this->_ptr);
|
||||
}
|
||||
|
||||
constexpr CURLU* get() const noexcept {
|
||||
return this->_ptr;
|
||||
}
|
||||
|
||||
CurlStr get(CURLUPart part, unsigned int flags = 0) const {
|
||||
char* content;
|
||||
CURLUcode code = curl_url_get(this->_ptr, part, &content, flags);
|
||||
if (code) {
|
||||
throw CurlUrlException(code);
|
||||
}
|
||||
|
||||
return CurlStr(content, curl_free);
|
||||
}
|
||||
|
||||
void set(CURLUPart part, const char* content, unsigned int flags = 0) {
|
||||
CURLUcode code = curl_url_set(this->_ptr, part, content, flags);
|
||||
if (code) {
|
||||
throw CurlUrlException(code);
|
||||
}
|
||||
}
|
||||
|
||||
void set(CURLUPart part, const std::string& content, unsigned int flags = 0) {
|
||||
this->set(part, content.c_str(), flags);
|
||||
}
|
||||
|
||||
private:
|
||||
CURLU* _ptr;
|
||||
};
|
8
main.cpp
8
main.cpp
|
@ -40,16 +40,20 @@ int main(int argc, char** argv) {
|
|||
atexit(MastodonClient::cleanup);
|
||||
|
||||
httplib::Server server;
|
||||
server.set_payload_max_length(8192);
|
||||
|
||||
server.Get("/", home_route);
|
||||
server.Get("/style.css", css_route);
|
||||
server.Get("/settings", user_settings_route);
|
||||
server.Post("/settings", user_settings_route);
|
||||
|
||||
server.Get("/(" DOMAIN_RE ")/@(" ACCT_RE ")(|/with_replies|/media)", user_route);
|
||||
server.Get("/(" DOMAIN_RE ")/users/(" ACCT_RE ")(|/with_replies|/media)", [](const httplib::Request& req, httplib::Response& res) {
|
||||
serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1) + "/@" + req.matches.str(2) + req.matches.str(3), true);
|
||||
});
|
||||
|
||||
server.Get("/(" DOMAIN_RE ")/@" ACCT_RE "/(\\d+)", status_route);
|
||||
server.Get("/(" DOMAIN_RE ")/users/(" ACCT_RE ")/statuses/(\\d+)", [](const httplib::Request& req, httplib::Response& res) {
|
||||
server.Get("/(" DOMAIN_RE ")/@" ACCT_RE "/([a-zA-Z0-9]+)", status_route);
|
||||
server.Get("/(" DOMAIN_RE ")/users/(" ACCT_RE ")/statuses/([a-zA-Z0-9]+)", [](const httplib::Request& req, httplib::Response& res) {
|
||||
serve_redirect(req, res, get_origin(req) + '/' + req.matches.str(1) + "/@" + req.matches.str(2) + '/' + req.matches.str(3), true);
|
||||
});
|
||||
|
||||
|
|
10
models.cpp
10
models.cpp
|
@ -19,7 +19,9 @@ void from_json(const json& j, Emoji& emoji) {
|
|||
void from_json(const json& j, AccountField& field) {
|
||||
j.at("name").get_to(field.name);
|
||||
field.value_html = j.at("value").get<std::string>();
|
||||
if (j.at("verified_at").is_null()) {
|
||||
// https://social.kernel.org/@monsieuricon/Ac6oYwtLhess6uil1c
|
||||
// https://social.kernel.org/@monsieuricon/Ac8RXJRqxTfUrM1xQG
|
||||
if (!j.contains("verified_at") || j["verified_at"].is_null()) {
|
||||
field.verified_at = -1;
|
||||
} else {
|
||||
field.verified_at = parse_rfc3339(j["verified_at"].get_ref<const std::string&>());
|
||||
|
@ -104,7 +106,8 @@ void from_json(const json& j, Post& post) {
|
|||
j.at("replies_count").get_to(post.replies_count);
|
||||
j.at("reblogs_count").get_to(post.reblogs_count);
|
||||
j.at("favourites_count").get_to(post.favorites_count);
|
||||
if (!j.at("edited_at").is_null()) {
|
||||
// https://social.kernel.org/@monsieuricon/Ac6oYwtLhess6uil1c
|
||||
if (j.contains("edited_at") && !j["edited_at"].is_null()) {
|
||||
post.edited_at = parse_rfc3339(j["edited_at"].get_ref<const std::string&>());
|
||||
} else {
|
||||
post.edited_at = -1;
|
||||
|
@ -117,7 +120,8 @@ void from_json(const json& j, Post& post) {
|
|||
j.at("account").get_to(post.account);
|
||||
j.at("media_attachments").get_to(post.media_attachments);
|
||||
j.at("emojis").get_to(post.emojis);
|
||||
if (!j.at("poll").is_null()) {
|
||||
// https://social.kernel.org/@monsieuricon/Ac6oYwtLhess6uil1c
|
||||
if (j.contains("poll") && !j["poll"].is_null()) {
|
||||
post.poll = j["poll"].get<Poll>();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,6 +14,10 @@ static const constexpr char css[] = R"EOF(
|
|||
--error-border-color: red;
|
||||
--error-text-color: white;
|
||||
|
||||
--success-background-color: green;
|
||||
--success-border-color: lightgreen;
|
||||
--success-text-color: white;
|
||||
|
||||
--accent-color: #962AC3;
|
||||
--bright-accent-color: #DE6DE6;
|
||||
}
|
||||
|
@ -22,9 +26,11 @@ static const constexpr char css[] = R"EOF(
|
|||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
p, details {
|
||||
/* .quote-inline and display: block: workaround for vt.social/@lina?max_id=111463476293964258 */
|
||||
p, details, .quote-inline {
|
||||
margin-top: 1em;
|
||||
margin-bottom: 1em;
|
||||
display: block;
|
||||
}
|
||||
ul, ol {
|
||||
margin: revert;
|
||||
|
@ -161,6 +167,20 @@ svg {
|
|||
padding: revert;
|
||||
}
|
||||
|
||||
/* SUCCESS BOX */
|
||||
.success {
|
||||
text-align: center;
|
||||
background-color: var(--success-background-color);
|
||||
color: var(--success-text-color);
|
||||
border-style: solid;
|
||||
border-color: var(--success-border-color);
|
||||
margin-bottom: 0.5em;
|
||||
}
|
||||
.success * {
|
||||
margin: revert;
|
||||
padding: revert;
|
||||
}
|
||||
|
||||
/* USER PAGE */
|
||||
.user_page-header {
|
||||
width: 100%;
|
||||
|
@ -250,6 +270,18 @@ svg {
|
|||
margin: revert;
|
||||
padding: revert;
|
||||
}
|
||||
|
||||
/* USER SETTINGS PAGE */
|
||||
.user_settings_page-form {
|
||||
display: inline;
|
||||
}
|
||||
.user_settings_page-form input[type=submit] {
|
||||
margin-top: 0.5em;
|
||||
padding: revert;
|
||||
}
|
||||
.user_settings_page-form .cancel {
|
||||
margin-left: 0.25em;
|
||||
}
|
||||
)EOF";
|
||||
// one for \0, one for trailing newline
|
||||
#define CSS_LEN sizeof(css) / sizeof(css[0]) - 2
|
||||
|
|
|
@ -8,6 +8,7 @@ void home_route(const httplib::Request& req, httplib::Response& res) {
|
|||
"their posts, posts themselves (and their context), posts with a certain hashtag, "
|
||||
"and instance details.",
|
||||
}),
|
||||
Element("p", {Element("a", {{"href", get_origin(req) + "/settings"}}, {"Configure settings"})}),
|
||||
Element("h2", {"Example links"}),
|
||||
Element("ul", {
|
||||
Element("li", {Element("a", {{"href", get_origin(req) + "/vt.social/@lina"}}, {"Asahi Lina (朝日リナ) // nullptr::live"})}),
|
||||
|
|
|
@ -10,3 +10,4 @@ void user_route(const httplib::Request& req, httplib::Response& res);
|
|||
void status_route(const httplib::Request& req, httplib::Response& res);
|
||||
void tags_route(const httplib::Request& req, httplib::Response& res);
|
||||
void about_route(const httplib::Request& req, httplib::Response& res);
|
||||
void user_settings_route(const httplib::Request& req, httplib::Response& res);
|
||||
|
|
|
@ -11,11 +11,6 @@ void tags_route(const httplib::Request& req, httplib::Response& res) {
|
|||
std::optional<std::string> max_id;
|
||||
if (req.has_param("max_id")) {
|
||||
max_id = req.get_param_value("max_id");
|
||||
if (max_id->empty() || max_id->find_first_not_of("0123456789") != std::string::npos) {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "Invalid max_id query paramter");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<Post> posts;
|
||||
|
|
|
@ -22,11 +22,6 @@ void user_route(const httplib::Request& req, httplib::Response& res) {
|
|||
std::optional<std::string> max_id;
|
||||
if (req.has_param("max_id")) {
|
||||
max_id = req.get_param_value("max_id");
|
||||
if (max_id->empty() || max_id->find_first_not_of("0123456789") != std::string::npos) {
|
||||
res.status = 400;
|
||||
serve_error(req, res, "400: Bad Request", "Invalid max_id query paramter");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<Account> account;
|
||||
|
@ -87,6 +82,14 @@ static inline PostSortingMethod get_sorting_method(const std::string& method) {
|
|||
|
||||
|
||||
static inline Element user_header(const httplib::Request& req, const std::string& server, const Account& account, PostSortingMethod sorting_method) {
|
||||
blankie::html::HTMLString preprocessed_bio = preprocess_html(req, account.server, account.emojis, account.note_html);
|
||||
// Workaround for https://social.kernel.org/@monsieuricon
|
||||
if (preprocessed_bio.str.find("<p>") == std::string::npos) {
|
||||
preprocessed_bio.str.reserve(preprocessed_bio.str.size() + 3 + 4);
|
||||
preprocessed_bio.str.insert(0, "<p>");
|
||||
preprocessed_bio.str.append("</p>");
|
||||
}
|
||||
|
||||
Element user_links("table", {{"class", "user_page-user_links"}}, {});
|
||||
user_links.nodes.reserve(account.fields.size());
|
||||
for (const AccountField& i : account.fields) {
|
||||
|
@ -118,7 +121,7 @@ static inline Element user_header(const httplib::Request& req, const std::string
|
|||
}),
|
||||
|
||||
Element("div", {{"class", "user_page-user_description"}}, {
|
||||
Element("div", {{"class", "user_page-user_bio"}}, {preprocess_html(req, account.server, account.emojis, account.note_html)}),
|
||||
Element("div", {{"class", "user_page-user_bio"}}, {std::move(preprocessed_bio)}),
|
||||
|
||||
std::move(user_links),
|
||||
}),
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
#include "routes.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../settings.h"
|
||||
#include "../timeutils.h"
|
||||
#include "../curlu_wrapper.h"
|
||||
|
||||
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value);
|
||||
|
||||
|
||||
void user_settings_route(const httplib::Request& req, httplib::Response& res) {
|
||||
UserSettings settings;
|
||||
|
||||
if (req.method == "POST") {
|
||||
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);
|
||||
}
|
||||
|
||||
Element auto_open_cw_checkbox("input", {{"type", "checkbox"}, {"name", "auto-open-cw"}, {"value", "true"}}, {});
|
||||
if (settings.auto_open_cw) {
|
||||
auto_open_cw_checkbox.attributes.push_back({"checked", ""});
|
||||
}
|
||||
|
||||
Element body("body", {
|
||||
Element("form", {{"class", "user_settings_page-form"}, {"method", "post"}}, {
|
||||
Element("label", {
|
||||
std::move(auto_open_cw_checkbox),
|
||||
" Automatically open Content Warnings",
|
||||
}),
|
||||
|
||||
Element("br"),
|
||||
Element("input", {{"type", "submit"}, {"value", "Save"}}, {}),
|
||||
}),
|
||||
Element("form", {{"class", "user_settings_page-form"}, {"method", "get"}, {"action", get_origin(req)}}, {
|
||||
Element("input", {{"class", "cancel"}, {"type", "submit"}, {"value", "Cancel"}}, {}),
|
||||
}),
|
||||
});
|
||||
if (req.method == "POST") {
|
||||
body.nodes.insert(body.nodes.begin(), Element("div", {{"class", "success"}}, {
|
||||
Element("h3", {"Settings saved!"}),
|
||||
}));
|
||||
}
|
||||
|
||||
serve(req, res, "User settings", std::move(body));
|
||||
}
|
||||
|
||||
|
||||
static void set_cookie(const httplib::Request& req, httplib::Response& res, const char* key, std::string_view value) {
|
||||
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);
|
||||
if (strcmp(origin.get(CURLUPART_SCHEME).get(), "https") == 0) {
|
||||
header += "; Secure";
|
||||
}
|
||||
|
||||
res.set_header("Set-Cookie", header);
|
||||
}
|
106
servehelper.cpp
106
servehelper.cpp
|
@ -6,10 +6,12 @@
|
|||
|
||||
#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"
|
||||
|
||||
|
@ -27,30 +29,6 @@ static Element serialize_post(const httplib::Request& req, const std::string& se
|
|||
static inline Element serialize_media(const Media& media);
|
||||
static inline Element serialize_poll(const httplib::Request& req, const Poll& poll);
|
||||
|
||||
class CurlUrlException : public std::exception {
|
||||
public:
|
||||
CurlUrlException(CURLUcode code_) : code(code_) {
|
||||
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
|
||||
snprintf(this->_id_buf, 64, "curl url error %d", this->code);
|
||||
#endif
|
||||
}
|
||||
|
||||
const char* what() const noexcept {
|
||||
#if CURL_AT_LEAST_VERSION(7, 80, 0)
|
||||
return curl_url_strerror(this->code);
|
||||
#else
|
||||
return this->_id_buf;
|
||||
#endif
|
||||
}
|
||||
|
||||
CURLUcode code;
|
||||
|
||||
private:
|
||||
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
|
||||
char _id_buf[64];
|
||||
#endif
|
||||
};
|
||||
|
||||
|
||||
void serve(const httplib::Request& req, httplib::Response& res, std::string title, Element element, Nodes extra_head) {
|
||||
using namespace std::string_literals;
|
||||
|
@ -118,6 +96,21 @@ void serve_redirect(const httplib::Request& req, httplib::Response& res, std::st
|
|||
|
||||
|
||||
|
||||
bool starts_with(const CurlUrl& url, const CurlUrl& base) {
|
||||
if (strcmp(url.get(CURLUPART_SCHEME).get(), base.get(CURLUPART_SCHEME).get()) != 0) {
|
||||
return false;
|
||||
}
|
||||
if (strcmp(url.get(CURLUPART_HOST).get(), base.get(CURLUPART_HOST).get()) != 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
CurlStr url_path = url.get(CURLUPART_PATH);
|
||||
CurlStr base_path = base.get(CURLUPART_PATH);
|
||||
size_t base_path_len = strlen(base_path.get());
|
||||
return memcpy(url_path.get(), base_path.get(), base_path_len) == 0
|
||||
&& (url_path.get()[base_path_len] == '/' || url_path.get()[base_path_len] == '\0');
|
||||
}
|
||||
|
||||
std::string get_origin(const httplib::Request& req) {
|
||||
if (req.has_header("X-Canonical-Origin")) {
|
||||
return req.get_header_value("X-Canonical-Origin");
|
||||
|
@ -139,43 +132,28 @@ std::string get_origin(const httplib::Request& req) {
|
|||
}
|
||||
|
||||
std::string proxy_mastodon_url(const httplib::Request& req, const std::string& url_str) {
|
||||
using CurlStr = std::unique_ptr<char, decltype(&curl_free)>;
|
||||
CurlUrl url;
|
||||
url.set(CURLUPART_URL, url_str.c_str());
|
||||
|
||||
std::unique_ptr<CURLU, decltype(&curl_url_cleanup)> url(curl_url(), curl_url_cleanup);
|
||||
if (!url) {
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
std::string new_url = get_origin(req) + '/' + url.get(CURLUPART_HOST).get() + url.get(CURLUPART_PATH).get();
|
||||
|
||||
// in a block to avoid a (potential) gcc bug where it thinks the lambda below
|
||||
// shadows `code`, even if i do `[&url](...) { ... }`
|
||||
{
|
||||
CURLUcode code = curl_url_set(url.get(), CURLUPART_URL, url_str.c_str(), 0);
|
||||
if (code) {
|
||||
throw CurlUrlException(code);
|
||||
}
|
||||
}
|
||||
|
||||
auto get_part = [&](CURLUPart part, CURLUcode ignore = CURLUE_OK) {
|
||||
char* content = nullptr;
|
||||
CURLUcode code = curl_url_get(url.get(), part, &content, 0);
|
||||
if (code && code != ignore) {
|
||||
throw CurlUrlException(code);
|
||||
}
|
||||
return CurlStr(content, curl_free);
|
||||
};
|
||||
CurlStr host = get_part(CURLUPART_HOST);
|
||||
CurlStr path = get_part(CURLUPART_PATH);
|
||||
CurlStr query = get_part(CURLUPART_QUERY, CURLUE_NO_QUERY);
|
||||
CurlStr fragment = get_part(CURLUPART_FRAGMENT, CURLUE_NO_FRAGMENT);
|
||||
|
||||
std::string new_url = get_origin(req) + '/' + host.get() + path.get();
|
||||
if (query) {
|
||||
try {
|
||||
CurlStr query = url.get(CURLUPART_QUERY);
|
||||
new_url += '?';
|
||||
new_url += query.get();
|
||||
} catch (const CurlUrlException& e) {
|
||||
if (e.code != CURLUE_NO_QUERY) {
|
||||
throw;
|
||||
}
|
||||
if (fragment) {
|
||||
}
|
||||
try {
|
||||
CurlStr fragment = url.get(CURLUPART_FRAGMENT);
|
||||
new_url += '#';
|
||||
new_url += fragment.get();
|
||||
} catch (const CurlUrlException& e) {
|
||||
if (e.code != CURLUE_NO_FRAGMENT) {
|
||||
throw;
|
||||
}
|
||||
}
|
||||
return new_url;
|
||||
}
|
||||
|
@ -263,10 +241,14 @@ static inline void preprocess_link(const httplib::Request& req, const std::strin
|
|||
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) : "";
|
||||
|
||||
std::string instance_url_base = "https://"s + domain_name;
|
||||
CurlUrl href_url;
|
||||
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) || href.starts_with(instance_url_base + '/') || href == instance_url_base) {
|
||||
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));
|
||||
|
||||
|
@ -387,7 +369,14 @@ static Element serialize_post(const httplib::Request& req, const std::string& se
|
|||
: "Created: "s + full_time(post.created_at) + "\nEdited: " + full_time(post.edited_at);
|
||||
const char* time_badge = post.edited_at < 0 ? "" : " (edited)";
|
||||
|
||||
Element contents("div", {{"class", "post-contents"}}, {preprocess_html(req, server, post.emojis, post.content)});
|
||||
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());
|
||||
|
@ -406,6 +395,9 @@ static Element serialize_post(const httplib::Request& req, const std::string& se
|
|||
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"}}, {
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
#include "blankie/serializer.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;
|
||||
|
@ -16,6 +17,7 @@ void serve_error(const httplib::Request& req, httplib::Response& res,
|
|||
std::string title, std::optional<std::string> subtitle = std::nullopt, std::optional<std::string> info = std::nullopt);
|
||||
void serve_redirect(const httplib::Request& req, httplib::Response& res, std::string url, bool permanent = false);
|
||||
|
||||
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);
|
||||
|
|
|
@ -0,0 +1,75 @@
|
|||
#include <string>
|
||||
#include <stdexcept>
|
||||
#include "settings.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);
|
||||
|
||||
|
||||
void UserSettings::set(std::string_view key, std::string_view value) {
|
||||
if (key == "auto-open-cw") {
|
||||
this->auto_open_cw = parse_bool(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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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;
|
||||
|
||||
if (value == "true") {
|
||||
return true;
|
||||
} else if (value == "false") {
|
||||
return false;
|
||||
} else {
|
||||
throw std::invalid_argument("unknown boolean value: "s + std::string(value));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,16 @@
|
|||
#pragma once
|
||||
|
||||
#include <string_view>
|
||||
#include <httplib/httplib.h>
|
||||
|
||||
struct UserSettings {
|
||||
bool auto_open_cw = false;
|
||||
|
||||
UserSettings() = default;
|
||||
UserSettings(const httplib::Request& req) {
|
||||
this->load_from_cookies(req);
|
||||
}
|
||||
|
||||
void set(std::string_view key, std::string_view value);
|
||||
void load_from_cookies(const httplib::Request& req);
|
||||
};
|
|
@ -1,3 +1,4 @@
|
|||
#include <cstring>
|
||||
#include <system_error>
|
||||
|
||||
#include "timeutils.h"
|
||||
|
@ -39,6 +40,19 @@ std::string to_rfc3339(time_t time) {
|
|||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
static const char* day_names[] = {"Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"};
|
||||
static const char* month_names[] = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"};
|
||||
std::string to_web_date(time_t time) {
|
||||
struct tm tm;
|
||||
gmtime_r(&time, &tm);
|
||||
|
||||
char buf[32];
|
||||
size_t len = strftime(buf, 32, "XXX, %d XXX %Y %H:%M:%S GMT", &tm);
|
||||
memcpy(buf, day_names[tm.tm_wday], 3);
|
||||
memcpy(buf + 3 + 2 + 2 + 1, month_names[tm.tm_mon], 3);
|
||||
return std::string(buf, len);
|
||||
}
|
||||
|
||||
std::string relative_time(time_t from, time_t to) {
|
||||
time_t diff = to - from;
|
||||
|
||||
|
|
|
@ -8,4 +8,5 @@ time_t current_time();
|
|||
std::string short_time(time_t time);
|
||||
std::string full_time(time_t time);
|
||||
std::string to_rfc3339(time_t time);
|
||||
std::string to_web_date(time_t time);
|
||||
std::string relative_time(time_t from, time_t to);
|
||||
|
|
Loading…
Reference in New Issue