Add primitive user page

This commit is contained in:
blankie 2023-04-05 23:36:20 +07:00
parent 1e4b221dd7
commit aecde91ad1
Signed by: blankie
GPG Key ID: CC15FC822C7F61F5
10 changed files with 165 additions and 28 deletions

View File

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

View File

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

View File

@ -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<const nlohmann::json::string_t&>());
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::string>();
std::optional<std::string> 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<std::string>();
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<std::string>();
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(

View File

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

View File

@ -2,7 +2,9 @@
#include <httplib/httplib.h>
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);

60
routes/users/common.cpp Normal file
View File

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

9
routes/users/common.h Normal file
View File

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

41
routes/users/users.cpp Normal file
View File

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

View File

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

View File

@ -13,3 +13,4 @@ void serve_error(const httplib::Request& req, httplib::Response& res, const Conf
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, 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);