imgurxp/src/imgurxp.cpp

239 lines
9.1 KiB
C++

#include <curl/curl.h>
#include <kore/kore.h>
#include <kore/http.h>
#include <kore/curl.h>
#include <kore/tasks.h>
#include <string>
#include "utils.hpp"
#include "config.hpp"
extern "C" {
int album_or_image(struct http_request*);
static int album_or_image_start(struct http_request*);
static int album_or_image_end(struct http_request*);
}
static struct http_state states[] = {
KORE_HTTP_STATE(album_or_image_start),
KORE_HTTP_STATE(album_or_image_end)
};
int album_or_image(struct http_request* req) {
return http_state_run(states, 2, req);
}
static bool send_hget_and_http_resp(struct http_request* req, const char* id, bool is_album) {
DeterminedKeys keys = determine_keys(id, is_album);
redis_mutex.lock();
redisReply* reply = (redisReply*)redisCommand(redis, "HGET %s %s", keys.key.c_str(), keys.field.c_str());
if (!reply) {
redis_mutex.unlock();
kore_log(LOG_WARNING, "received nullptr while sending HGET to redis");
return false;
}
if (reply->type == REDIS_REPLY_ERROR) {
kore_log(LOG_WARNING, "received error while sending HGET to redis: %s", reply->str);
freeReplyObject(reply);
redis_mutex.unlock();
return false;
}
if (reply->type == REDIS_REPLY_NIL) {
freeReplyObject(reply);
redis_mutex.unlock();
return false;
}
if (!reply->len) {
freeReplyObject(reply);
redis_mutex.unlock();
char* error = "404: media not found";
if (is_album) {
error = "404: album not found";
}
std::string error_page = build_error_page(error);
http_response_header(req, "Content-Type", "text/html");
http_response(req, 404, error_page.c_str(), error_page.length());
return true;
}
Album album = {false, std::nullopt, {}};
std::optional<std::string> error = unpack_album_from_string(&album, reply->str);
freeReplyObject(reply);
redis_mutex.unlock();
if (error) {
kore_log(LOG_WARNING, "received error while parsing packed album: %s", (*error).c_str());
return false;
}
send_album_page(req, std::move(album));
return true;
}
static int album_or_image_start(struct http_request* req) {
const char* id = &(req->path[1]);
bool is_album = false;
const char* slash_in_path = strchr(id, '/');
if (slash_in_path) {
id = &(slash_in_path[1]);
is_album = true;
}
if (redis) {
if (send_hget_and_http_resp(req, id, is_album)) {
return HTTP_STATE_COMPLETE;
}
}
std::string api_url = "https://api.imgur.com/post/v1/";
if (is_album) {
api_url.append("albums/");
} else {
api_url.append("media/");
}
api_url.append(id);
api_url.append("?client_id=546c25a59c58ad7&include=media");
struct kore_curl* client = (kore_curl*)http_state_create(req, sizeof(*client), NULL);
if (!kore_curl_init(client, api_url.c_str(), KORE_CURL_ASYNC)) {
http_state_cleanup(req);
kore_log(LOG_ERR, "failed to initialize curl client");
std::string error_page = build_error_page("Failed to initialize curl client");
http_response_header(req, "Content-Type", "text/html");
http_response(req, 500, error_page.c_str(), error_page.length());
return HTTP_STATE_COMPLETE;
}
// handle compressed responses automatically
curl_easy_setopt(client->handle, CURLOPT_ACCEPT_ENCODING, "");
kore_curl_http_setup(client, HTTP_METHOD_GET, NULL, 0);
kore_curl_bind_request(client, req);
kore_curl_run(client);
req->fsm_state = 1;
return HTTP_STATE_RETRY;
}
static void send_hset(const char* id, bool is_album, const char* packed) {
DeterminedKeys keys = determine_keys(id, is_album);
redis_mutex.lock();
redisReply* reply = (redisReply*)redisCommand(redis, "HSET %s %s %s", keys.key.c_str(), keys.field.c_str(), packed);
if (!reply) {
redis_mutex.unlock();
kore_log(LOG_WARNING, "received nullptr while sending HSET to redis");
return;
}
if (reply->type == REDIS_REPLY_ERROR) {
kore_log(LOG_WARNING, "received error while sending HSET to redis: %s", reply->str);
freeReplyObject(reply);
redis_mutex.unlock();
return;
}
freeReplyObject(reply);
redis_mutex.unlock();
return;
}
static int album_or_image_end(struct http_request* req) {
const char* id = &(req->path[1]);
bool is_album = false;
const char* slash_in_path = strchr(id, '/');
if (slash_in_path) {
id = &(slash_in_path[1]);
is_album = true;
}
struct kore_curl* client = (kore_curl*)http_state_get(req);
if (!kore_curl_success(client)) {
kore_curl_logerror(client);
std::string error = "Failed to contact API: ";
error.append(kore_curl_strerror(client));
kore_curl_cleanup(client);
http_state_cleanup(req);
std::string error_page = build_error_page(error.c_str());
http_response_header(req, "Content-Type", "text/html");
http_response(req, 500, error_page.c_str(), error_page.length());
return HTTP_STATE_COMPLETE;
}
char* body = kore_curl_response_as_string(client);
kore_json json;
kore_json_init(&json, body, strlen(body));
if (kore_json_parse(&json) != KORE_RESULT_OK) {
kore_log(LOG_ERR, "failed to parse json: %s", kore_json_strerror(&json));
std::string error = "Failed to parse response: ";
error.append(kore_json_strerror(&json));
kore_json_cleanup(&json);
kore_curl_cleanup(client);
http_state_cleanup(req);
std::string error_page = build_error_page(error.c_str());
http_response_header(req, "Content-Type", "text/html");
http_response(req, 500, error_page.c_str(), error_page.length());
return HTTP_STATE_COMPLETE;
}
kore_json_item* errors = kore_json_find_array(json.root, "errors");
if (errors) {
std::string error = "Received error(s) from API:<pre><code>";
struct kore_json_item* error_json;
if (!TAILQ_EMPTY(&errors->data.items)) {
error_json = TAILQ_FIRST(&errors->data.items);
kore_json_item* error_code_json = kore_json_find_string(error_json, "code");
kore_json_item* error_detail_json = kore_json_find_string(error_json, "detail");
if (error_code_json && error_detail_json && !strcmp(error_code_json->data.string, "404")) {
error = "404: ";
error.append(error_detail_json->data.string);
kore_json_cleanup(&json);
kore_curl_cleanup(client);
http_state_cleanup(req);
if (redis) {
send_hset(id, is_album, "");
}
std::string error_page = build_error_page(error.c_str());
http_response_header(req, "Content-Type", "text/html");
http_response(req, 404, error_page.c_str(), error_page.length());
return HTTP_STATE_COMPLETE;
}
}
bool needs_newline = false;
TAILQ_FOREACH(error_json, &errors->data.items, list) {
// istg if string.append mutates strings passed
char* error_code = "unknown error code";
char* error_detail = "unknown error detail";
kore_json_item* error_code_json = kore_json_find_string(error_json, "code");
kore_json_item* error_detail_json = kore_json_find_string(error_json, "detail");
if (error_code_json) {
error_code = error_code_json->data.string;
}
if (error_detail_json) {
error_detail = error_detail_json->data.string;
}
kore_log(LOG_ERR, "received error from api: %s: %s", error_code, error_detail);
std::string to_append;
if (needs_newline) {
to_append.append("\n");
}
to_append.append(error_code);
to_append.append(": ");
to_append.append(error_detail);
escape_xml(&to_append);
error.append(std::move(to_append));
needs_newline = true;
}
error.append("</code></pre>");
kore_json_cleanup(&json);
kore_curl_cleanup(client);
http_state_cleanup(req);
std::string error_page = build_error_page(error.c_str(), true);
http_response_header(req, "Content-Type", "text/html");
http_response(req, 500, error_page.c_str(), error_page.length());
return HTTP_STATE_COMPLETE;
}
Album album;
std::optional<std::string> error = deserialize_album(json, &album);
kore_json_cleanup(&json);
kore_curl_cleanup(client);
http_state_cleanup(req);
if (error) {
kore_log(LOG_ERR, "%s", (*error).c_str());
std::string error_page = build_error_page((*error).c_str());
http_response_header(req, "Content-Type", "text/html");
http_response(req, 500, error_page.c_str(), error_page.length());
return HTTP_STATE_COMPLETE;
}
if (redis) {
std::string packed = pack_album_into_string(album);
send_hset(id, is_album, packed.c_str());
}
send_album_page(req, std::move(album));
return HTTP_STATE_COMPLETE;
}