Add the option to automatically open content warnings

This commit is contained in:
blankie 2023-11-30 16:33:17 +11:00
parent e231afb49c
commit 402e74dd6e
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
11 changed files with 212 additions and 2 deletions

View File

@ -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

View File

@ -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);

View File

@ -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

View File

@ -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"})}),

View File

@ -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);

64
routes/user_settings.cpp Normal file
View File

@ -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);
}

View File

@ -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"}}, {

75
settings.cpp Normal file
View File

@ -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));
}
}

16
settings.h Normal file
View File

@ -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);
};

View File

@ -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;

View File

@ -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);