Add the option to automatically open content warnings
This commit is contained in:
		
							parent
							
								
									e231afb49c
								
							
						
					
					
						commit
						402e74dd6e
					
				| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
							
								
								
									
										4
									
								
								main.cpp
								
								
								
								
							
							
						
						
									
										4
									
								
								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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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);
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -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"}}, {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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