From aecde91ad11c5ff8dd9487ce12759aed190b8064 Mon Sep 17 00:00:00 2001 From: blankie Date: Wed, 5 Apr 2023 23:36:20 +0700 Subject: [PATCH] Add primitive user page --- CMakeLists.txt | 2 +- main.cpp | 15 +++-------- pixivclient.cpp | 20 +++++++------- routes/css.cpp | 18 +++++++++++++ routes/routes.h | 4 ++- routes/users/common.cpp | 60 +++++++++++++++++++++++++++++++++++++++++ routes/users/common.h | 9 +++++++ routes/users/users.cpp | 41 ++++++++++++++++++++++++++++ servehelper.cpp | 23 ++++++++++++---- servehelper.h | 1 + 10 files changed, 165 insertions(+), 28 deletions(-) create mode 100644 routes/users/common.cpp create mode 100644 routes/users/common.h create mode 100644 routes/users/users.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index e83ece5..eaf8ae8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -24,7 +24,7 @@ add_link_options(${FLAGS}) add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp pixivclient.cpp blankie/serializer.cpp blankie/escape.cpp - routes/home.cpp routes/css.cpp) + routes/home.cpp routes/css.cpp routes/users/common.cpp routes/users/users.cpp) set_target_properties(${PROJECT_NAME} PROPERTIES CXX_STANDARD 20 diff --git a/main.cpp b/main.cpp index 1c340cb..36e01d3 100644 --- a/main.cpp +++ b/main.cpp @@ -28,6 +28,10 @@ int main(int argc, char** argv) { }); server.Get("/style\\.css", css_route); + server.Get("/users/(\\d+)", [&](const httplib::Request& req, httplib::Response& res) { + users_route(req, res, config, pixiv_client); + }); + server.Get("/member\\.php", [&](const httplib::Request& req, httplib::Response& res) { std::string id = req.get_param_value("id"); if (id.empty() || id.find_first_not_of("1234567890") != std::string::npos) { @@ -54,17 +58,6 @@ int main(int argc, char** argv) { server.Get("/debug/exception/unknown", [](const httplib::Request& req, httplib::Response& res) { throw "cope"; }); - // TODO stop it - server.Get("/debug/getuser/good", [&](const httplib::Request& req, httplib::Response& res) { - User user = pixiv_client.get_user(2583663); - printf("%s\n", user.username.c_str()); - res.set_content(user.username, "text/plain"); - }); - server.Get("/debug/getuser/bad", [&](const httplib::Request& req, httplib::Response& res) { - User user = pixiv_client.get_user(1); - printf("%s\n", user.username.c_str()); - res.set_content(user.username, "text/plain"); - }); #endif server.Get(".*", [&](const httplib::Request& req, httplib::Response& res) { res.status = 404; diff --git a/pixivclient.cpp b/pixivclient.cpp index 2a057ae..1dade6a 100644 --- a/pixivclient.cpp +++ b/pixivclient.cpp @@ -38,7 +38,7 @@ void from_json(const nlohmann::json& j, User& user) { j.at("user_name").get_to(user.display_name); user.user_id = to_ull(j.at("user_id").get_ref()); - if (j.contains("cover_image")) { + if (j.contains("cover_image") && j["cover_image"].is_object()) { nlohmann::json cover_image = j["cover_image"]; std::string c_720x360 = cover_image.at("profile_cover_image").at("720x360").get(); std::optional original = get_original_cover_image(c_720x360); @@ -59,9 +59,9 @@ void from_json(const nlohmann::json& j, User& user) { std::string user_webpage = j.at("user_webpage").get(); if (!user_webpage.empty()) { - user.links.push_back({"webpage", std::move(user_webpage)}); + user.links.push_back({"Webpage", std::move(user_webpage)}); } - auto add_social_as_needed = [&](const char* key) { + auto add_social_as_needed = [&](const char* key, const char* public_name) { nlohmann::json social = j.at("social"); if (!social.is_object()) { return; @@ -71,14 +71,14 @@ void from_json(const nlohmann::json& j, User& user) { } std::string url = social[key].at("url").get(); - user.links.push_back({key, std::move(url)}); + user.links.push_back({public_name, std::move(url)}); }; - add_social_as_needed("twitter"); - add_social_as_needed("instagram"); - add_social_as_needed("tumblr"); - add_social_as_needed("facebook"); - add_social_as_needed("circlems"); - add_social_as_needed("pawoo"); + add_social_as_needed("twitter", "Twitter"); + add_social_as_needed("instagram", "Instagram"); + add_social_as_needed("tumblr", "Tumblr"); + add_social_as_needed("facebook", "Facebook"); + add_social_as_needed("circlems", "Circle.ms"); + add_social_as_needed("pawoo", "Pawoo"); } static std::regex c1920x960_cover_image_thumbnail_regex( diff --git a/routes/css.cpp b/routes/css.cpp index 8738026..4e94d42 100644 --- a/routes/css.cpp +++ b/routes/css.cpp @@ -31,6 +31,24 @@ void css_route(const httplib::Request& req, httplib::Response& res) { text-decoration: underline; } + .cover { + object-fit: cover; + width: 100%; + height: 50vh; + margin-bottom: 1em; + } + .profilepicture { + object-fit: cover; + width: 5em; + height: 5em; + margin-right: .5em; + } + .usermetadata { + display: flex; + align-items: center; + margin-left: .5em; + } + .error { text-align: center; background-color: var(--error-background-color); diff --git a/routes/routes.h b/routes/routes.h index 49454dc..6041d63 100644 --- a/routes/routes.h +++ b/routes/routes.h @@ -2,7 +2,9 @@ #include -struct Config; // forward declaration from config.h +struct Config; // forward declaration from ../config.h +class PixivClient; // forward declaration from ../pixivclient.h void home_route(const httplib::Request& req, httplib::Response& res, const Config& config); void css_route(const httplib::Request& req, httplib::Response& res); +void users_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client); diff --git a/routes/users/common.cpp b/routes/users/common.cpp new file mode 100644 index 0000000..540bdf8 --- /dev/null +++ b/routes/users/common.cpp @@ -0,0 +1,60 @@ +#include "common.h" +#include "../../config.h" +#include "../../servehelper.h" +#include "../../pixivclient.h" + +static inline Element generate_user_links(const User& user); +static std::string thumbnail_or_original(const Images& images); +static std::string original_or_thumbnail(const Images& images); + +Element generate_user_header(const User& user, const Config& config) { + auto proxy_url = [&](std::string url) { + return config.image_proxy_url + remove_origin(std::move(url)); + }; + + Element header("header"); + if (user.cover_images) { + header.nodes.push_back(Element("a", {{"href", proxy_url(original_or_thumbnail(*user.cover_images))}}, { + Element("img", {{"class", "cover"}, {"src", proxy_url(thumbnail_or_original(*user.cover_images))}}, {}) + })); + } + + header.nodes.push_back(Element("div", {{"class", "usermetadata"}}, { + Element("a", {{"href", proxy_url(original_or_thumbnail(user.profile_pictures))}}, { + Element("img", {{"class", "profilepicture"}, {"src", proxy_url(thumbnail_or_original(user.profile_pictures))}}, {}) + }), + Element("div", { + Element("p", {Element("b", {user.display_name}), " (@", user.username, ")"}), + generate_user_links(user) + }) + })); + return header; +} + +static inline Element generate_user_links(const User& user) { + Element p("p"); + for (const auto &[name, url] : user.links) { + if (!p.nodes.empty()) { + p.nodes.push_back(", "); + } + p.nodes.push_back(Element("a", {{"href", url}}, {name})); + } + return p; +} + +static std::string thumbnail_or_original(const Images& images) { + if (!images.thumbnails.empty()) { + return images.thumbnails.back(); + } + return images.original.value_or(""); +} + +static std::string original_or_thumbnail(const Images& images) { + if (images.original) { + return *images.original; + } + if (!images.thumbnails.empty()) { + return images.thumbnails.back(); + } + return ""; +} diff --git a/routes/users/common.h b/routes/users/common.h new file mode 100644 index 0000000..2a1c84d --- /dev/null +++ b/routes/users/common.h @@ -0,0 +1,9 @@ +#pragma once + +#include "../../blankie/serializer.h" +struct User; // forward declaration from ../../pixivclient.h +struct Config; // forward declaration from ../../config.h + +using Element = blankie::html::Element; + +Element generate_user_header(const User& user, const Config& config); diff --git a/routes/users/users.cpp b/routes/users/users.cpp new file mode 100644 index 0000000..ae8d376 --- /dev/null +++ b/routes/users/users.cpp @@ -0,0 +1,41 @@ +#include "../routes.h" +#include "../../servehelper.h" +#include "../../pixivclient.h" +#include "common.h" + +static inline uint64_t to_ull(const std::string& str); + +void users_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client) { + uint64_t user_id = to_ull(req.matches[1].str()); + User user; + try { + user = pixiv_client.get_user(user_id); + } catch (const PixivException& e) { + if (e.status == 404) { + res.status = 404; + serve_error(req, res, config, "404: User not found", e.what()); + } else { + res.status = 500; + serve_error(req, res, config, "500: Internal server error", "Failed to fetch user information", e.what()); + } + return; + } catch (const std::exception& e) { + res.status = 500; + serve_error(req, res, config, "500: Internal server error", "Failed to fetch user information", e.what()); + return; + } + serve(req, res, config, user.display_name + " (@" + user.username + ')', generate_user_header(user, config)); +} + +static inline uint64_t to_ull(const std::string& str) { + char* endptr; + + errno = 0; + uint64_t res = strtoull(str.c_str(), &endptr, 10); + if (res == ULLONG_MAX && errno == ERANGE) { + throw std::overflow_error(str + " is too big"); + } else if (endptr[0] != '\0') { + throw std::invalid_argument(str + " contains trailing text or is not an integer"); + } + return res; +} diff --git a/servehelper.cpp b/servehelper.cpp index fb00969..bc0c8e0 100644 --- a/servehelper.cpp +++ b/servehelper.cpp @@ -3,11 +3,6 @@ #include "config.h" #include "servehelper.h" -static std::regex image_proxy_regex( - "(https?://)?" // optional scheme - "(?:.+?@)?" // optional username and pass - "([^/]+(?::\\d+)?)" // host - "(?:/.*)?$"); static inline std::string get_image_proxy_origin(const std::string& url); void serve(const httplib::Request& req, httplib::Response& res, const Config& config, std::string title, Element element) { @@ -79,7 +74,25 @@ std::string get_origin(const httplib::Request& req, const Config& config) { return origin; } +static std::regex remove_origin_regex( + "(?:https?://)?" // optional schema + "(?:.+?@)?" // optional username and pass + "(?:[^/]+[.:][^/]+(?:\\d+)?)" // host + "(/.*)"); +std::string remove_origin(const std::string& url) { + std::smatch sm; + if (!std::regex_match(url, sm, remove_origin_regex)) { + return url; + } + return sm[1].str(); +} + +static std::regex image_proxy_regex( + "(https?://)?" // optional scheme + "(?:.+?@)?" // optional username and pass + "([^/]+(?::\\d+)?)" // host + "(?:/.*)?$"); static inline std::string get_image_proxy_origin(const std::string& url) { std::smatch sm; if (!std::regex_match(url, sm, image_proxy_regex)) { diff --git a/servehelper.h b/servehelper.h index ee55b26..26b7e9f 100644 --- a/servehelper.h +++ b/servehelper.h @@ -13,3 +13,4 @@ void serve_error(const httplib::Request& req, httplib::Response& res, const Conf std::string title, std::optional subtitle = std::nullopt, std::optional info = std::nullopt); void serve_redirect(const httplib::Request& req, httplib::Response& res, const Config& config, std::string url); std::string get_origin(const httplib::Request& req, const Config& config); +std::string remove_origin(const std::string& url);