Add redis support
This commit is contained in:
parent
773f75543e
commit
109556f2b7
|
@ -1,11 +1,14 @@
|
|||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
project(pixwhile CXX)
|
||||
project(pixwhile C CXX)
|
||||
|
||||
|
||||
find_package(nlohmann_json REQUIRED)
|
||||
set(HTTPLIB_REQUIRE_OPENSSL ON)
|
||||
add_subdirectory(thirdparty/httplib)
|
||||
find_package(PkgConfig REQUIRED)
|
||||
pkg_check_modules(HIREDIS REQUIRED hiredis)
|
||||
|
||||
|
||||
if (CMAKE_BUILD_TYPE MATCHES "Debug")
|
||||
if (NOT FLAGS)
|
||||
|
@ -23,15 +26,15 @@ list(APPEND FLAGS -Werror -Wall -Wextra -Wshadow -Wpedantic -Wno-gnu-anonymous-s
|
|||
add_link_options(${FLAGS})
|
||||
|
||||
|
||||
add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp numberhelper.cpp pixivclient.cpp pixivmodels.cpp
|
||||
blankie/serializer.cpp blankie/escape.cpp blankie/murl.cpp
|
||||
routes/home.cpp routes/css.cpp routes/artworks.cpp routes/tags.cpp routes/users/common.cpp routes/users/illustrations.cpp)
|
||||
add_executable(${PROJECT_NAME} main.cpp misc.cpp config.cpp servehelper.cpp numberhelper.cpp pixivclient.cpp pixivmodels.cpp hiredis_wrapper.cpp
|
||||
routes/home.cpp routes/css.cpp routes/artworks.cpp routes/tags.cpp routes/guess_extension.cpp routes/users/common.cpp routes/users/illustrations.cpp
|
||||
blankie/serializer.cpp blankie/escape.cpp blankie/murl.cpp)
|
||||
set_target_properties(${PROJECT_NAME}
|
||||
PROPERTIES
|
||||
CXX_STANDARD 20
|
||||
CXX_STANDARD_REQUIRED YES
|
||||
CXX_EXTENSIONS NO
|
||||
)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE thirdparty)
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json httplib::httplib)
|
||||
target_include_directories(${PROJECT_NAME} PRIVATE thirdparty ${HIREDIS_INCLUDE_DIRS})
|
||||
target_link_libraries(${PROJECT_NAME} PRIVATE nlohmann_json::nlohmann_json httplib::httplib ${HIREDIS_LINK_LIBRARIES})
|
||||
target_compile_options(${PROJECT_NAME} PRIVATE ${FLAGS})
|
||||
|
|
|
@ -17,6 +17,15 @@ liking. Here's a list of what they are:
|
|||
- `bind_port` (zero or positive integer): What port to bind to
|
||||
- `image_proxy_url` (string): URL to proxy images to (see https://pixiv.cat/reverseproxy.html), a trailing slash is not needed
|
||||
- `canonical_origin` (string or null): A fallback canonical origin if set, useful if you're, say, running Pixwhile behind Ngrok
|
||||
- `redis` (object)
|
||||
- `enabled` (boolean)
|
||||
- `connection` (object)
|
||||
- `type` ("ip" or "unix"): Whether to connect to redis via TCP or UNIX domain sockets
|
||||
- `address` (string or null): If `type` is "ip", the address to connect to. Ignored otherwise
|
||||
- `port` (int or null): If `type` is "ip", the port to connect to. Ignored otherwise
|
||||
- `unix` (string or null): If `type` is "unix", the path to redis' socket. Ignored otherwise
|
||||
- `username` (string or null): Optional username for authentication
|
||||
- `password` (string or null): Optional password for authentication
|
||||
|
||||
Pixwhile is intended to be run behind a reverse proxy (e.g. Nginx), so you
|
||||
should set your reverse proxy to proxy requests to
|
||||
|
|
31
config.cpp
31
config.cpp
|
@ -8,6 +8,33 @@ Config load_config(const char* path) {
|
|||
return nlohmann::json::parse(config_file.get());
|
||||
}
|
||||
|
||||
RedisConfig get_redis_config(const nlohmann::json& j) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
RedisConfig redis_config;
|
||||
const nlohmann::json& connection = j.at("connection");
|
||||
const std::string& connection_type = connection.at("type").get_ref<const std::string&>();
|
||||
if (connection_type == "ip") {
|
||||
redis_config.connection_method = IPConnection{
|
||||
connection.at("address").get<std::string>(),
|
||||
connection.at("port").get<int>()
|
||||
};
|
||||
} else if (connection_type == "unix") {
|
||||
redis_config.connection_method = UnixConnection{connection.at("unix").get<std::string>()};
|
||||
} else {
|
||||
throw std::invalid_argument("Unknown redis connection type: "s + connection_type);
|
||||
}
|
||||
|
||||
if (j.at("username").is_string()) {
|
||||
redis_config.username = j["username"].get<std::string>();
|
||||
}
|
||||
if (j.at("password").is_string()) {
|
||||
redis_config.password = j["password"].get<std::string>();
|
||||
}
|
||||
|
||||
return redis_config;
|
||||
}
|
||||
|
||||
void from_json(const nlohmann::json& j, Config& config) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
|
@ -20,4 +47,8 @@ void from_json(const nlohmann::json& j, Config& config) {
|
|||
if (j.contains("canonical_origin") && j["canonical_origin"].is_string()) {
|
||||
config.canonical_origin = j["canonical_origin"].get<std::string>();
|
||||
}
|
||||
|
||||
if (j.contains("redis") && j["redis"].at("enabled")) {
|
||||
config.redis_config = get_redis_config(j["redis"]);
|
||||
}
|
||||
}
|
||||
|
|
16
config.h
16
config.h
|
@ -1,14 +1,30 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <variant>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include "blankie/murl.h"
|
||||
|
||||
struct IPConnection {
|
||||
std::string address;
|
||||
int port;
|
||||
};
|
||||
struct UnixConnection {
|
||||
std::string unix;
|
||||
};
|
||||
|
||||
struct RedisConfig {
|
||||
std::variant<IPConnection, UnixConnection> connection_method;
|
||||
std::optional<std::string> username;
|
||||
std::optional<std::string> password;
|
||||
};
|
||||
|
||||
struct Config {
|
||||
std::string bind_host = "127.0.0.1";
|
||||
int bind_port = 8080;
|
||||
blankie::murl::Url image_proxy_url{"https://i.pixiv.cat"};
|
||||
std::optional<std::string> canonical_origin;
|
||||
std::optional<RedisConfig> redis_config;
|
||||
};
|
||||
|
||||
Config load_config(const char* path);
|
||||
|
|
|
@ -2,5 +2,16 @@
|
|||
"bind_host": "127.0.0.1",
|
||||
"bind_port": 8080,
|
||||
"image_proxy_url": "https://i.pixiv.cat",
|
||||
"canonical_origin": null
|
||||
"canonical_origin": null,
|
||||
"redis": {
|
||||
"enabled": true,
|
||||
"connection": {
|
||||
"type": "ip",
|
||||
"address": "127.0.0.1",
|
||||
"port": 6389,
|
||||
"unix": "/path/to/redis.sock"
|
||||
},
|
||||
"username": null,
|
||||
"password": "one of the passwords of all time"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,128 @@
|
|||
#include "hiredis_wrapper.h"
|
||||
|
||||
Redis::Redis(const std::string& address, int port) {
|
||||
this->_context = redisConnect(address.c_str(), port);
|
||||
if (!this->_context) {
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
if (this->_context->err) {
|
||||
RedisException e(this->_context->errstr);
|
||||
redisFree(this->_context);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Redis::Redis(const std::string& unix) {
|
||||
this->_context = redisConnectUnix(unix.c_str());
|
||||
if (!this->_context) {
|
||||
throw std::bad_alloc();
|
||||
}
|
||||
if (this->_context->err) {
|
||||
RedisException e(this->_context->errstr);
|
||||
redisFree(this->_context);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
Redis::~Redis() {
|
||||
redisFree(this->_context);
|
||||
}
|
||||
|
||||
void Redis::auth(const std::string& username, const std::string& password) {
|
||||
RedisReply reply = this->command("AUTH %s %s", username.c_str(), password.c_str());
|
||||
if (reply->type == REDIS_REPLY_STATUS) {
|
||||
// good :D
|
||||
} else {
|
||||
throw std::runtime_error("AUTH gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
void Redis::auth(const std::string& password) {
|
||||
RedisReply reply = this->command("AUTH %s", password.c_str());
|
||||
if (reply->type == REDIS_REPLY_STATUS) {
|
||||
// good :D
|
||||
} else {
|
||||
throw std::runtime_error("AUTH gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
|
||||
time_t Redis::ttl(const std::string& key) {
|
||||
RedisReply reply = this->command("TTL %s", key.c_str());
|
||||
if (reply->type == REDIS_REPLY_INTEGER) {
|
||||
return reply->integer;
|
||||
} else {
|
||||
throw std::runtime_error("TTL gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
|
||||
bool Redis::expire(const std::string& key, time_t expiry) {
|
||||
RedisReply reply = this->command("EXPIRE %s %d", key.c_str(), expiry);
|
||||
|
||||
if (reply->type == REDIS_REPLY_INTEGER) {
|
||||
return reply->integer == 1;
|
||||
} else {
|
||||
throw std::runtime_error("EXPIRE gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
|
||||
bool Redis::expire_nx(const std::string& key, time_t expiry) {
|
||||
if (this->_fake_expire_nx) {
|
||||
time_t current_expiry = this->ttl(key);
|
||||
if (current_expiry < 0) {
|
||||
return this->expire(key, expiry);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
RedisReply reply(nullptr, freeReplyObject);
|
||||
try{
|
||||
reply = this->command("EXPIRE %s %d NX", key.c_str(), expiry);
|
||||
} catch (const RedisException& e) {
|
||||
if (e.error == "ERR wrong number of arguments for 'expire' command") {
|
||||
this->_fake_expire_nx = true;
|
||||
return this->expire_nx(key, expiry);
|
||||
}
|
||||
throw;
|
||||
}
|
||||
|
||||
if (reply->type == REDIS_REPLY_INTEGER) {
|
||||
return reply->integer == 1;
|
||||
} else {
|
||||
throw std::runtime_error("EXPIRE NX gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> Redis::get(const std::string& key) {
|
||||
RedisReply reply = this->command("GET %s", key.c_str());
|
||||
if (reply->type == REDIS_REPLY_STRING) {
|
||||
return std::string(reply->str, reply->len);
|
||||
} else if (reply->type == REDIS_REPLY_NIL) {
|
||||
return std::nullopt;
|
||||
} else {
|
||||
throw std::runtime_error("GET gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
void Redis::set(const std::string& key, const std::string& value, time_t expiry) {
|
||||
RedisReply reply = this->command("SET %s %s EX %d", key.c_str(), value.c_str(), expiry);
|
||||
if (reply->type == REDIS_REPLY_STATUS) {
|
||||
// good :D
|
||||
} else {
|
||||
throw std::runtime_error("SET gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
|
||||
std::optional<std::string> Redis::hget(const std::string& key, const std::string& field) {
|
||||
RedisReply reply = this->command("HGET %s %s", key.c_str(), field.c_str());
|
||||
if (reply->type == REDIS_REPLY_STRING) {
|
||||
return std::string(reply->str, reply->len);
|
||||
} else if (reply->type == REDIS_REPLY_NIL) {
|
||||
return std::nullopt;
|
||||
} else {
|
||||
throw std::runtime_error("HGET gave an unexpected return type");
|
||||
}
|
||||
}
|
||||
void Redis::hset(const std::string& key, const std::string& field, const std::string& value) {
|
||||
RedisReply reply = this->command("HSET %s %s %s", key.c_str(), field.c_str(), value.c_str());
|
||||
if (reply->type == REDIS_REPLY_INTEGER) {
|
||||
// good :D
|
||||
} else {
|
||||
throw std::runtime_error("SET gave an unexpected return type");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,70 @@
|
|||
#pragma once
|
||||
|
||||
#include <string>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <exception>
|
||||
|
||||
#pragma GCC diagnostic push
|
||||
#pragma GCC diagnostic ignored "-Wc99-extensions"
|
||||
#pragma GCC diagnostic ignored "-Wconversion"
|
||||
#include <hiredis.h>
|
||||
#pragma GCC diagnostic pop
|
||||
|
||||
class RedisException : public std::exception {
|
||||
public:
|
||||
RedisException(const char* error_) : error(error_) {}
|
||||
RedisException(const char* error_, size_t length) : error(error_, length) {}
|
||||
|
||||
const char* what() const noexcept {
|
||||
return this->error.c_str();
|
||||
}
|
||||
|
||||
std::string error;
|
||||
};
|
||||
|
||||
using RedisReply = std::unique_ptr<redisReply, decltype(&freeReplyObject)>;
|
||||
|
||||
class Redis {
|
||||
public:
|
||||
Redis(const Redis&) = delete;
|
||||
Redis operator=(const Redis&) = delete;
|
||||
|
||||
Redis(const std::string& address, int port);
|
||||
Redis(const std::string& unix);
|
||||
~Redis();
|
||||
|
||||
template<typename... Args>
|
||||
RedisReply command(const char* format, Args... args) {
|
||||
std::lock_guard<std::mutex> guard(this->_mutex);
|
||||
redisReply* raw_reply = static_cast<redisReply*>(redisCommand(this->_context, format, args...));
|
||||
if (!raw_reply) {
|
||||
throw RedisException(this->_context->errstr);
|
||||
}
|
||||
|
||||
RedisReply reply(raw_reply, freeReplyObject);
|
||||
if (reply->type == REDIS_REPLY_ERROR) {
|
||||
throw RedisException(reply->str, reply->len);
|
||||
}
|
||||
return reply;
|
||||
}
|
||||
|
||||
void auth(const std::string& username, const std::string& password);
|
||||
void auth(const std::string& password);
|
||||
|
||||
time_t ttl(const std::string& key);
|
||||
bool expire(const std::string& key, time_t expiry);
|
||||
bool expire_nx(const std::string& key, time_t expiry);
|
||||
|
||||
std::optional<std::string> get(const std::string& key);
|
||||
void set(const std::string& key, const std::string& value, time_t expiry);
|
||||
|
||||
std::optional<std::string> hget(const std::string& key, const std::string& field);
|
||||
void hset(const std::string& key, const std::string& field, const std::string& value);
|
||||
|
||||
private:
|
||||
redisContext* _context;
|
||||
std::mutex _mutex;
|
||||
|
||||
bool _fake_expire_nx = false;
|
||||
};
|
36
main.cpp
36
main.cpp
|
@ -6,6 +6,7 @@
|
|||
#include "pixivclient.h"
|
||||
#include "servehelper.h"
|
||||
#include "routes/routes.h"
|
||||
#include "hiredis_wrapper.h"
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 2) {
|
||||
|
@ -20,9 +21,22 @@ int main(int argc, char** argv) {
|
|||
fprintf(stderr, "Failed to load config: %s\n", e.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
PixivClient pixiv_client;
|
||||
std::optional<Redis> redis;
|
||||
if (config.redis_config) {
|
||||
if (const IPConnection* ip_connection = std::get_if<IPConnection>(&config.redis_config->connection_method)) {
|
||||
redis.emplace(ip_connection->address, ip_connection->port);
|
||||
} else if (const UnixConnection* unix_connection = std::get_if<UnixConnection>(&config.redis_config->connection_method)) {
|
||||
redis.emplace(unix_connection->unix);
|
||||
}
|
||||
if (config.redis_config->username && config.redis_config->password) {
|
||||
redis->auth(*config.redis_config->username, *config.redis_config->password);
|
||||
} else if (config.redis_config->password) {
|
||||
redis->auth(*config.redis_config->password);
|
||||
}
|
||||
}
|
||||
PixivClient pixiv_client(redis ? &*redis : nullptr);
|
||||
httplib::Server server;
|
||||
|
||||
server.Get("/", [&](const httplib::Request& req, httplib::Response& res) {
|
||||
home_route(req, res, config);
|
||||
});
|
||||
|
@ -73,23 +87,7 @@ int main(int argc, char** argv) {
|
|||
});
|
||||
|
||||
server.Get("/guess_extension/i.pximg.net(/.+)", [&](const httplib::Request& req, httplib::Response& res) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
// rip query parameters, but they're not important anyway
|
||||
std::string path = req.matches.str(1);
|
||||
auto serve_extension = [&](const char* extension) {
|
||||
if (!pixiv_client.i_pximg_url_valid(path + '.' + extension)) {
|
||||
return false;
|
||||
}
|
||||
serve_redirect(req, res, config, proxy_image_url(req, config, "https://i.pximg.net" + path + '.' + extension), true);
|
||||
return true;
|
||||
};
|
||||
|
||||
if (serve_extension("png") || serve_extension("jpg")) {
|
||||
return;
|
||||
}
|
||||
res.status = 500;
|
||||
serve_error(req, res, config, "500: Internal Server Error", "Failed to guess file extension for https://i.pximg.net"s + path);
|
||||
guess_extension_route(req, res, config, pixiv_client, redis ? &*redis : nullptr);
|
||||
});
|
||||
|
||||
#ifndef NDEBUG
|
||||
|
|
103
pixivclient.cpp
103
pixivclient.cpp
|
@ -1,11 +1,16 @@
|
|||
#include <FastHash.h>
|
||||
|
||||
#include "blankie/murl.h"
|
||||
#include "numberhelper.h"
|
||||
#include "pixivclient.h"
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
static const constexpr char* touch_user_agent = "Mozilla/5.0 (Android 12; Mobile; rv:97.0) Gecko/97.0 Firefox/97.0";
|
||||
static const constexpr char* desktop_user_agent = "Mozilla/5.0 (Windows NT 10.0; rv:111.0) Gecko/20100101 Firefox/111.0";
|
||||
static void to_lower(std::string& str);
|
||||
|
||||
PixivClient::PixivClient() {
|
||||
PixivClient::PixivClient(Redis* redis) : _redis(redis) {
|
||||
this->_www_pixiv_net_client.set_keep_alive(true);
|
||||
this->_www_pixiv_net_client.set_default_headers({
|
||||
{"Cookie", "webp_available=1"}
|
||||
|
@ -17,10 +22,9 @@ PixivClient::PixivClient() {
|
|||
}
|
||||
|
||||
User PixivClient::get_user(uint64_t user_id) {
|
||||
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/details", {
|
||||
{"lang", "en"}, {"id", std::to_string(user_id)}
|
||||
}, {{"User-Agent", touch_user_agent}});
|
||||
return this->_handle_result(std::move(res)).at("user_details").get<User>();
|
||||
nlohmann::json j = this->_call_api("pixwhile:user:"s + std::to_string(user_id), std::nullopt, 24 * 60 * 60,
|
||||
"/touch/ajax/user/details", {{"lang", "en"}, {"id", std::to_string(user_id)}}, {{"User-Agent", touch_user_agent}});
|
||||
return j.at("user_details").get<User>();
|
||||
}
|
||||
|
||||
Illusts PixivClient::get_illusts(uint64_t user_id, size_t page) {
|
||||
|
@ -28,21 +32,21 @@ Illusts PixivClient::get_illusts(uint64_t user_id, size_t page) {
|
|||
if (page != 0) {
|
||||
params.insert({"p", std::to_string(page + 1)});
|
||||
}
|
||||
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/user/illusts", std::move(params), {{"User-Agent", touch_user_agent}});
|
||||
return this->_handle_result(std::move(res)).get<Illusts>();
|
||||
|
||||
nlohmann::json j = this->_call_api("pixwhile:illusts:"s + std::to_string(user_id), std::to_string(page), 60 * 60,
|
||||
"/touch/ajax/user/illusts", std::move(params), {{"User-Agent", touch_user_agent}});
|
||||
return j.get<Illusts>();
|
||||
}
|
||||
|
||||
Illust PixivClient::get_illust(uint64_t illust_id) {
|
||||
httplib::Result res = this->_www_pixiv_net_client.Get("/touch/ajax/illust/details", {
|
||||
{"lang", "en"}, {"illust_id", std::to_string(illust_id)}
|
||||
}, {{"User-Agent", touch_user_agent}});
|
||||
return this->_handle_result(std::move(res)).get<Illust>();
|
||||
nlohmann::json j = this->_call_api("pixwhile:illust:"s + std::to_string(illust_id), std::nullopt, 24 * 60 * 60,
|
||||
"/touch/ajax/illust/details", {{"lang", "en"}, {"illust_id", std::to_string(illust_id)}}, {{"User-Agent", touch_user_agent}});
|
||||
return j.get<Illust>();
|
||||
}
|
||||
|
||||
SearchResults PixivClient::search_illusts(const std::string& query, size_t page, const std::string& order) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
httplib::Result res = this->_www_pixiv_net_client.Get("/ajax/search/illustrations/"s + blankie::murl::escape(query), {
|
||||
SearchResults PixivClient::search_illusts(std::string query, size_t page, const std::string& order) {
|
||||
to_lower(query);
|
||||
httplib::Params params = {
|
||||
{"lang", "en"},
|
||||
{"mode", "all"},
|
||||
{"p", std::to_string(page + 1)},
|
||||
|
@ -50,18 +54,34 @@ SearchResults PixivClient::search_illusts(const std::string& query, size_t page,
|
|||
{"type", "illust_and_ugoira"},
|
||||
{"order", order},
|
||||
{"word", query}
|
||||
}, {{"User-Agent", desktop_user_agent}});
|
||||
return this->_handle_result(std::move(res)).get<SearchResults>();
|
||||
};
|
||||
|
||||
std::string cache_key = "pixwhile:search:"s + order + ':' + std::to_string(FastHash(query.data(), query.size(), 0));
|
||||
nlohmann::json j = this->_call_api(std::move(cache_key), std::to_string(page), 60 * 60,
|
||||
"/ajax/search/illustrations/"s + blankie::murl::escape(std::move(query)), std::move(params), {{"User-Agent", desktop_user_agent}});
|
||||
return j.get<SearchResults>();
|
||||
}
|
||||
|
||||
std::vector<SearchSuggestion> PixivClient::get_search_suggestions(const std::string& query) {
|
||||
httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", {
|
||||
{"lang", "en"}, {"keyword", query}
|
||||
}, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}});
|
||||
if (!res) {
|
||||
throw HTTPLibException(res.error());
|
||||
}
|
||||
nlohmann::json j = nlohmann::json::parse(std::move(res->body)).at("candidates");
|
||||
std::vector<SearchSuggestion> PixivClient::get_search_suggestions(std::string query) {
|
||||
to_lower(query);
|
||||
std::string body = [&]() {
|
||||
std::string cache_key = "pixwhile:searchsugg:"s + std::to_string(FastHash(query.data(), query.size(), 0));
|
||||
std::optional<std::string> cached_body = this->_redis->get(cache_key);
|
||||
if (cached_body) {
|
||||
return std::move(*cached_body);
|
||||
}
|
||||
|
||||
httplib::Result res = this->_www_pixiv_net_client.Get("/rpc/cps.php", {
|
||||
{"lang", "en"}, {"keyword", query}
|
||||
}, {{"User-Agent", desktop_user_agent}, {"Referer", "https://www.pixiv.net/"}});
|
||||
if (!res) {
|
||||
throw HTTPLibException(res.error());
|
||||
}
|
||||
|
||||
this->_redis->set(std::move(cache_key), res->body, 24 * 60 * 60);
|
||||
return std::move(res->body);
|
||||
}();
|
||||
nlohmann::json j = nlohmann::json::parse(std::move(body)).at("candidates");
|
||||
|
||||
std::vector<SearchSuggestion> search_suggestions;
|
||||
search_suggestions.reserve(j.size());
|
||||
|
@ -76,16 +96,34 @@ std::vector<SearchSuggestion> PixivClient::get_search_suggestions(const std::str
|
|||
return search_suggestions;
|
||||
}
|
||||
|
||||
nlohmann::json PixivClient::_handle_result(httplib::Result res) {
|
||||
nlohmann::json PixivClient::_call_api(std::string cache_key, std::optional<std::string> cache_field, time_t expiry,
|
||||
std::string path, httplib::Params params, httplib::Headers headers) {
|
||||
std::optional<std::string> success_body = !cache_field
|
||||
? this->_redis->get(cache_key)
|
||||
: this->_redis->hget(cache_key, *cache_field);
|
||||
if (success_body) {
|
||||
return nlohmann::json::parse(std::move(*success_body));
|
||||
}
|
||||
|
||||
httplib::Result res = this->_www_pixiv_net_client.Get(std::move(path), std::move(params), std::move(headers));
|
||||
if (!res) {
|
||||
throw HTTPLibException(res.error());
|
||||
}
|
||||
|
||||
nlohmann::json j = nlohmann::json::parse(std::move(res->body));
|
||||
if (j.at("error")) {
|
||||
throw PixivException(res->status, j.at("message").get<std::string>());
|
||||
}
|
||||
return j.at("body");
|
||||
|
||||
j = j.at("body");
|
||||
// trim data sent to redis without making our own serialization of our objects
|
||||
j.erase("ads");
|
||||
if (!cache_field) {
|
||||
this->_redis->set(std::move(cache_key), j.dump(), expiry);
|
||||
} else {
|
||||
this->_redis->hset(cache_key, std::move(*cache_field), j.dump());
|
||||
this->_redis->expire_nx(std::move(cache_key), expiry);
|
||||
}
|
||||
return j;
|
||||
}
|
||||
|
||||
bool PixivClient::i_pximg_url_valid(const std::string& path) {
|
||||
|
@ -95,3 +133,12 @@ bool PixivClient::i_pximg_url_valid(const std::string& path) {
|
|||
}
|
||||
return res->status >= 200 && res->status <= 200;
|
||||
}
|
||||
|
||||
|
||||
static void to_lower(std::string& str) {
|
||||
for (size_t i = 0; i < str.size(); i++) {
|
||||
if (str[i] >= 'A' && str[i] <= 'Z') {
|
||||
str[i] = str[i] - 'A' + 'a';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,28 +1,31 @@
|
|||
#pragma once
|
||||
|
||||
#include <vector>
|
||||
|
||||
#include "pixivmodels.h"
|
||||
#include <httplib/httplib.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include "pixivmodels.h"
|
||||
#include "hiredis_wrapper.h"
|
||||
|
||||
class PixivClient {
|
||||
public:
|
||||
PixivClient();
|
||||
PixivClient(Redis* redis);
|
||||
|
||||
User get_user(uint64_t user_id);
|
||||
Illusts get_illusts(uint64_t user_id, size_t page);
|
||||
Illust get_illust(uint64_t illust_id);
|
||||
|
||||
SearchResults search_illusts(const std::string& query, size_t page, const std::string& order);
|
||||
std::vector<SearchSuggestion> get_search_suggestions(const std::string& query);
|
||||
SearchResults search_illusts(std::string query, size_t page, const std::string& order);
|
||||
std::vector<SearchSuggestion> get_search_suggestions(std::string query);
|
||||
|
||||
bool i_pximg_url_valid(const std::string& path);
|
||||
|
||||
private:
|
||||
nlohmann::json _handle_result(httplib::Result res);
|
||||
nlohmann::json _call_api(std::string cache_key, std::optional<std::string> cache_field, time_t expiry,
|
||||
std::string path, httplib::Params params, httplib::Headers headers);
|
||||
httplib::Client _www_pixiv_net_client{"https://www.pixiv.net"};
|
||||
httplib::Client _i_pximg_net_client{"https://i.pximg.net"};
|
||||
Redis* _redis;
|
||||
};
|
||||
|
||||
class HTTPLibException : public std::exception {
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
#include <FastHash.h>
|
||||
|
||||
#include "routes.h"
|
||||
#include "../servehelper.h"
|
||||
#include "../pixivclient.h"
|
||||
#include "../hiredis_wrapper.h"
|
||||
|
||||
static inline std::optional<std::string> guess_extension(const std::string& path, PixivClient& pixiv_client);
|
||||
|
||||
void guess_extension_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client, Redis* redis) {
|
||||
using namespace std::string_literals;
|
||||
|
||||
std::string path = req.matches.str(1);
|
||||
std::string cache_key = "pixwhile:i.pximg.net:ext:"s + std::to_string(FastHash(path.data(), path.size(), 0));
|
||||
|
||||
std::optional<std::string> extension = redis
|
||||
? redis->get(cache_key)
|
||||
: std::nullopt;
|
||||
if (!extension) {
|
||||
extension = guess_extension(path, pixiv_client);
|
||||
if (!extension) {
|
||||
res.status = 500;
|
||||
serve_error(req, res, config, "500: Internal Server Error", "Failed to guess file extension for https://i.pximg.net"s + path);
|
||||
return;
|
||||
}
|
||||
if (redis) {
|
||||
redis->set(std::move(cache_key), *extension, 24 * 60 * 60);
|
||||
}
|
||||
}
|
||||
|
||||
serve_redirect(req, res, config, proxy_image_url(req, config, "https://i.pximg.net" + path + '.' + *extension), true);
|
||||
}
|
||||
|
||||
static inline std::optional<std::string> guess_extension(const std::string& path, PixivClient& pixiv_client) {
|
||||
auto guess_extension = [&](const char* extension) {
|
||||
return pixiv_client.i_pximg_url_valid(path + '.' + extension);
|
||||
};
|
||||
|
||||
if (guess_extension("png")) {
|
||||
return "png";
|
||||
} else if (guess_extension("jpg")) {
|
||||
return "jpg";
|
||||
} else {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
struct Config; // forward declaration from ../config.h
|
||||
class PixivClient; // forward declaration from ../pixivclient.h
|
||||
class Redis; // forward declaration from ../hiredis_wrapper.h
|
||||
|
||||
extern const uint64_t css_hash;
|
||||
|
||||
|
@ -12,3 +13,4 @@ void css_route(const httplib::Request& req, httplib::Response& res);
|
|||
void user_illustrations_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client);
|
||||
void artworks_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client);
|
||||
void tags_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client);
|
||||
void guess_extension_route(const httplib::Request& req, httplib::Response& res, const Config& config, PixivClient& pixiv_client, Redis* redis);
|
||||
|
|
|
@ -48,7 +48,7 @@
|
|||
// (h) *= 0x2127599bf4325c37ULL;
|
||||
// (h) ^= (h) >> 47; })
|
||||
|
||||
constexpr uint64_t FastHash(const char* str, size_t size, uint64_t seed/*, uint64_t back = {}*/)
|
||||
constexpr uint64_t FastHash(const char* str, std::size_t size, uint64_t seed/*, uint64_t back = {}*/)
|
||||
{
|
||||
auto const mix = [](uint64_t h)
|
||||
{
|
||||
|
@ -62,7 +62,7 @@ constexpr uint64_t FastHash(const char* str, size_t size, uint64_t seed/*, uint6
|
|||
|
||||
uint64_t h = seed ^ (size * m);
|
||||
|
||||
for (size_t i = 0; i < size; i++)
|
||||
for (std::size_t i = 0; i < size; i++)
|
||||
{
|
||||
h ^= mix(static_cast<uint64_t>(str[i]));
|
||||
h *= m;
|
||||
|
|
Loading…
Reference in New Issue