diff --git a/CMakeLists.txt b/CMakeLists.txt index 1117cfa..2c70b1d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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 diff --git a/main.cpp b/main.cpp index fc60f0e..ece043f 100644 --- a/main.cpp +++ b/main.cpp @@ -40,9 +40,13 @@ 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); diff --git a/routes/css.cpp b/routes/css.cpp index d6939e9..0fb7013 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -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; } @@ -163,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%; @@ -252,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 diff --git a/routes/home.cpp b/routes/home.cpp index 0a1f053..5443c26 100644 --- a/routes/home.cpp +++ b/routes/home.cpp @@ -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"})}), diff --git a/routes/routes.h b/routes/routes.h index a38898f..b91e042 100644 --- a/routes/routes.h +++ b/routes/routes.h @@ -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); diff --git a/routes/user_settings.cpp b/routes/user_settings.cpp new file mode 100644 index 0000000..f57866b --- /dev/null +++ b/routes/user_settings.cpp @@ -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); +} diff --git a/servehelper.cpp b/servehelper.cpp index 8a526d3..1a613b8 100644 --- a/servehelper.cpp +++ b/servehelper.cpp @@ -6,6 +6,7 @@ #include "font_awesome.h" #include "config.h" +#include "settings.h" #include "models.h" #include "timeutils.h" #include "servehelper.h" @@ -394,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"}}, { diff --git a/settings.cpp b/settings.cpp new file mode 100644 index 0000000..9890c2d --- /dev/null +++ b/settings.cpp @@ -0,0 +1,75 @@ +#include +#include +#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)); + } +} diff --git a/settings.h b/settings.h new file mode 100644 index 0000000..c20cf59 --- /dev/null +++ b/settings.h @@ -0,0 +1,16 @@ +#pragma once + +#include +#include + +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); +}; diff --git a/timeutils.cpp b/timeutils.cpp index c6daf0b..d0fc608 100644 --- a/timeutils.cpp +++ b/timeutils.cpp @@ -1,3 +1,4 @@ +#include #include #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; diff --git a/timeutils.h b/timeutils.h index 35ae574..7fd3343 100644 --- a/timeutils.h +++ b/timeutils.h @@ -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);