diff --git a/.gitmodules b/.gitmodules index 10beb96..6a71dfc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "docs/doxygen-awesome-css"] path = docs/doxygen-awesome-css url = https://github.com/jothepro/doxygen-awesome-css.git +[submodule "deps/drogon"] + path = deps/drogon + url = https://github.com/drogonframework/drogon.git diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d97a2b..a6034d8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,30 +1,46 @@ -cmake_minimum_required(VERSION 3.8.2) +cmake_minimum_required(VERSION 3.15) project( topgg LANGUAGES CXX HOMEPAGE_URL "https://docs.top.gg/docs" - DESCRIPTION "The official C++ wrapper for the Top.gg API." + DESCRIPTION "A simple API wrapper for Top.gg written in C++." ) -set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type") +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type") + option(BUILD_SHARED_LIBS "Build shared libraries" ON) -option(ENABLE_CORO "Support for C++20 coroutines" OFF) +option(ENABLE_API "Build primary API support" ON) +option(ENABLE_CPP_HTTPLIB_WEBHOOKS "Build support for webhooks via cpp-httplib" OFF) +option(ENABLE_DROGON_WEBHOOKS "Build support for webhooks via drogon" OFF) +option(ENABLE_CORO "Add support for C++20 coroutines" OFF) +option(TESTING "Enable this only if you are testing the library" OFF) +if(ENABLE_API) file(GLOB TOPGG_SOURCE_FILES src/*.cpp) +endif() + +if(ENABLE_CPP_HTTPLIB_WEBHOOKS) +set(TOPGG_SOURCE_FILES ${TOPGG_SOURCE_FILES} src/webhooks/cpp-httplib.cpp src/webhooks/models.cpp) +elseif(ENABLE_DROGON_WEBHOOKS) +set(TOPGG_SOURCE_FILES ${TOPGG_SOURCE_FILES} src/webhooks/drogon.cpp src/webhooks/models.cpp) +endif() if(BUILD_SHARED_LIBS) add_library(topgg SHARED ${TOPGG_SOURCE_FILES}) if(WIN32) -target_sources(topgg PRIVATE ${CMAKE_SOURCE_DIR}/topgg.rc) +target_sources(topgg PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/topgg.rc) +target_compile_definitions(topgg PRIVATE __TOPGG_BUILDING_DLL__) endif() else() add_library(topgg STATIC ${TOPGG_SOURCE_FILES}) -endif() if(WIN32) -target_compile_definitions(topgg PRIVATE $<$:__TOPGG_BUILDING_DLL__:DPP_STATIC TOPGG_STATIC>) +target_compile_definitions(topgg PUBLIC DPP_STATIC TOPGG_STATIC) +endif() endif() if(ENABLE_CORO) @@ -34,26 +50,95 @@ else() set(TOPGG_CXX_STANDARD 17) endif() +if(TESTING) +target_compile_definitions(topgg PUBLIC __TOPGG_TESTING__) +endif() + set_target_properties(topgg PROPERTIES - OUTPUT_NAME topgg CXX_STANDARD ${TOPGG_CXX_STANDARD} CXX_STANDARD_REQUIRED ON ) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) -set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +if(ENABLE_API) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) find_package(DPP REQUIRED) +endif() + +if(ENABLE_CPP_HTTPLIB_WEBHOOKS) +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib/cpp-httplib.h") +execute_process(COMMAND git clone https://github.com/yhirose/cpp-httplib.git --depth 1 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib") +file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib") +endif() +file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/cpp-httplib/httplib.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib/cpp-httplib.h") +file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/cpp-httplib") +endif() + +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann/json.hpp") +execute_process(COMMAND git clone https://github.com/nlohmann/json.git --depth 1 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann") +file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann") +endif() +file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/json/single_include/nlohmann/json.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann/json.hpp") +file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/json") +endif() +endif() + +if(ENABLE_DROGON_WEBHOOKS) +if(WIN32) +set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/conan_toolchain.cmake") + +include(${CMAKE_CURRENT_SOURCE_DIR}/conan_toolchain.cmake) +endif() + +set(DROGON_LIBRARY drogon) + +set( + TRANTOR_INCLUDE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/trantor" + "${CMAKE_BINARY_DIR}/deps/drogon/trantor/exports" +) + +set( + DROGON_INCLUDE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/lib/inc" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/orm_lib/inc" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/nosql_lib/redis/inc" + "${CMAKE_BINARY_DIR}/deps/drogon/exports" +) + +set(BUILD_CTL OFF) +set(BUILD_EXAMPLES OFF) +set(BUILD_BROTLI OFF) +set(BUILD_YAML_CONFIG OFF) +set(USE_SUBMODULE ON) + +add_subdirectory(deps/drogon) + +target_compile_definitions(topgg PUBLIC __TOPGG_DROGON_WEBHOOKS__) + +if(WIN32) +target_compile_definitions(topgg PRIVATE _CRT_SECURE_NO_WARNINGS) +cmake_policy(SET CMP0091 NEW) +endif() +endif() + +target_compile_definitions(topgg PRIVATE __TOPGG_BUILDING__) if(MSVC) -target_compile_options(topgg PUBLIC $<$:/diagnostics:caret /MTd> $<$:/MT /O2 /Oi /Oy /Gy>) +target_compile_options(topgg PRIVATE /nologo $<$:/diagnostics:caret /MDd /DDEBUG /D_DEBUG> $<$:/MD /O2 /Oi /Oy /Gy /DNDEBUG>) else() -target_compile_options(topgg PUBLIC $<$:-O3> -Wall -Wextra -Wpedantic -Wformat=2 -Wnull-dereference -Wuninitialized -Wdeprecated) +target_compile_options(topgg PRIVATE $<$:-O3> -Wall -Wextra -Wpedantic -Wformat=2 -Wnull-dereference -Wuninitialized -Wdeprecated) endif() target_include_directories(topgg PUBLIC - ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/include ${DPP_INCLUDE_DIR} + ${JSONCPP_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIR} + ${TRANTOR_INCLUDE_DIR} + ${DROGON_INCLUDE_DIR} ) -target_link_libraries(topgg ${DPP_LIBRARIES}) \ No newline at end of file +target_link_libraries(topgg PUBLIC ${DPP_LIBRARIES} ${JSONCPP_LIBRARIES} ${DROGON_LIBRARY}) \ No newline at end of file diff --git a/conanfile.txt b/conanfile.txt new file mode 100644 index 0000000..5d648d2 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,3 @@ +[requires] +jsoncpp/1.9.4 +zlib/1.2.11 diff --git a/deps/drogon b/deps/drogon new file mode 160000 index 0000000..065899d --- /dev/null +++ b/deps/drogon @@ -0,0 +1 @@ +Subproject commit 065899df98b8ec8ede94328d03ce94fedec6d6e0 diff --git a/include/topgg/webhooks/cpp-httplib.h b/include/topgg/webhooks/cpp-httplib.h new file mode 100644 index 0000000..d2f9dd4 --- /dev/null +++ b/include/topgg/webhooks/cpp-httplib.h @@ -0,0 +1,82 @@ +/** + * @module topgg + * @file cpp-httplib.h + * @brief A community-maintained C++ API Client for the Top.gg API. + * @authors Top.gg, null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-10-02 + * @version 2.1.0 + */ + +#pragma once + +#ifndef CPPHTTPLIB_HTTPLIB_H +#include +#endif + +#include + +#include +#include +#include +#include + +namespace topgg { + namespace webhook { + class internal_cpp_httplib { + protected: + const std::string m_authorization; + + std::optional parse(const httplib::Request& request, httplib::Response& response) const noexcept; + + inline internal_cpp_httplib(const std::string& authorization): m_authorization(authorization) {} + + public: + internal_cpp_httplib() = delete; + }; + + using cpp_httplib_endpoint = std::function; + + /** + * @brief An abstract class that receives a Top.gg webhook event. Designed for use in cpp-httplib. + * + * @see topgg::webhook::vote_event + * @since 2.1.0 + */ + template + class cpp_httplib: private internal_cpp_httplib { + public: + cpp_httplib() = delete; + + inline cpp_httplib(const std::string& authorization): internal_cpp_httplib(authorization) { + static_assert(std::is_constructible_v, "T must be a valid model class."); + } + + /** + * @brief Returns the endpoint callback to be used in a cpp-httplib server route. + * + * @return cpp_httplib_endpoint The endpoint callback. + * @since 2.1.0 + */ + inline cpp_httplib_endpoint endpoint() noexcept { + return [this](const httplib::Request& request, httplib::Response& response) { + const auto json_data{this->parse(request, response)}; + + if (json_data.has_value()) { + const T data{json_data.value()}; + + this->callback(data); + } + }; + } + + /** + * @brief The virtual callback that will be called whenever an incoming webhook request to the endpoint has been authenticated. + * + * @param T data The webhook event data. + * @since 2.1.0 + */ + virtual void callback(const T& data) = 0; + }; + }; // namespace webhook +}; // namespace topgg \ No newline at end of file diff --git a/include/topgg/webhooks/drogon.h b/include/topgg/webhooks/drogon.h new file mode 100644 index 0000000..a92eafb --- /dev/null +++ b/include/topgg/webhooks/drogon.h @@ -0,0 +1,84 @@ +/** + * @module topgg + * @file drogon.h + * @brief A community-maintained C++ API Client for the Top.gg API. + * @authors Top.gg, null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-10-02 + * @version 2.1.0 + */ + +#pragma once + +#ifndef __TOPGG_DROGON_WEBHOOKS__ +#define __TOPGG_DROGON_WEBHOOKS__ +#endif + +#include + +#include + +#include +#include +#include +#include + +#define TOPGG_DROGON_WEBHOOK() \ + void asyncHandleHttpRequest(const ::drogon::HttpRequestPtr& request, std::function&& response) override { \ + __handle(request, std::forward>(response)); \ + } + +namespace topgg { + namespace webhook { + class internal_drogon { + protected: + const std::string m_authorization; + + std::optional parse(const ::drogon::HttpRequestPtr& request, const ::drogon::HttpResponsePtr& response) const noexcept; + + inline internal_drogon(const std::string& authorization): m_authorization(authorization) {} + + public: + internal_drogon() = delete; + }; + + /** + * @brief An abstract class that receives a Top.gg webhook event. Designed for use as a drogon::HttpSimpleController in drogon. + * + * @see topgg::webhook::vote_event + * @since 2.1.0 + */ + template + class drogon: private internal_drogon { + public: + drogon() = delete; + + inline drogon(const std::string& authorization): internal_drogon(authorization) { + static_assert(std::is_constructible_v, "T must be a valid model class."); + } + + void __handle(const ::drogon::HttpRequestPtr& request, std::function&& response) { + const auto response_data{::drogon::HttpResponse::newHttpResponse()}; + const auto json_data{parse(request, response_data)}; + + if (json_data.has_value()) { + const T data{json_data.value()}; + + callback(data); + } + + response(response_data); + } + + /** + * @brief The virtual callback that will be called whenever an incoming webhook request to the endpoint has been authenticated. + * + * @param T data The webhook event data. + * @since 2.1.0 + */ + virtual void callback(const T& data) = 0; + }; + }; // namespace webhook +}; // namespace topgg + +#undef __TOPGG_DROGON_WEBHOOKS__ \ No newline at end of file diff --git a/include/topgg/webhooks/models.h b/include/topgg/webhooks/models.h new file mode 100644 index 0000000..916acf4 --- /dev/null +++ b/include/topgg/webhooks/models.h @@ -0,0 +1,79 @@ +/** + * @module topgg + * @file models.h + * @brief A community-maintained C++ API Client for the Top.gg API. + * @authors Top.gg, null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-10-02 + * @version 2.1.0 + */ + +#pragma once + +#include + +#ifdef __TOPGG_DROGON_WEBHOOKS__ +#include +#else +#include +#endif + +#include +#include + +namespace topgg { + namespace webhook { +#ifdef __TOPGG_DROGON_WEBHOOKS__ + using json = Json::Value; +#else + using json = nlohmann::json; +#endif + + /** + * @brief A dispatched Top.gg vote webhook event. + * + * @since 2.1.0 + */ + class vote_event { + public: + TOPGG_EXPORT vote_event(const json& j); + + vote_event() = delete; + + /** + * @brief The ID of the project that received a vote. + * + * @since 2.1.0 + */ + std::string receiver_id; + + /** + * @brief The ID of the Top.gg user who voted. + * + * @since 2.1.0 + */ + std::string voter_id; + + /** + * @brief Whether this vote is just a test done from the page settings. + * + * @since 2.1.0 + */ + bool is_test; + + /** + * @brief Whether the weekend multiplier is active, where a single vote counts as two. + * + * @since 2.1.0 + */ + bool is_weekend; + + /** + * @brief Query strings found on the vote page. + * + * @since 2.1.0 + */ + std::unordered_map query; + }; + }; // namespace webhook +}; // namespace topgg \ No newline at end of file diff --git a/src/webhooks/cpp-httplib.cpp b/src/webhooks/cpp-httplib.cpp new file mode 100644 index 0000000..d9be493 --- /dev/null +++ b/src/webhooks/cpp-httplib.cpp @@ -0,0 +1,35 @@ +#include + +#include + +using namespace topgg::webhook; + +std::optional internal_cpp_httplib::parse(const httplib::Request& request, httplib::Response& response) const noexcept { + if (request.method != "POST") { + response.status = 405; + response.set_content("Method not allowed", "text/plain"); + + return std::nullopt; + } + + const auto authorization{request.headers.find("Authorization")}; + + if (authorization == request.headers.end() || authorization->second != m_authorization) { + response.status = 401; + response.set_content("Unauthorized", "text/plain"); + + return std::nullopt; + } + + try { + const auto json_body{json::parse(request.body)}; + response.status = 204; + + return json_body; + } catch (TOPGG_UNUSED const std::exception&) { + response.status = 400; + response.set_content("Invalid JSON body", "text/plain"); + + return std::nullopt; + } +} \ No newline at end of file diff --git a/src/webhooks/drogon.cpp b/src/webhooks/drogon.cpp new file mode 100644 index 0000000..e6420a2 --- /dev/null +++ b/src/webhooks/drogon.cpp @@ -0,0 +1,45 @@ +#include + +#include + +using namespace topgg; + +std::optional webhook::internal_drogon::parse(const ::drogon::HttpRequestPtr& request, const ::drogon::HttpResponsePtr& response) const noexcept { + if (request->getMethod() != ::drogon::Post) { + response->setStatusCode(::drogon::k405MethodNotAllowed); + response->setContentTypeCode(::drogon::CT_TEXT_PLAIN); + response->setBody("Method not allowed"); + + return std::nullopt; + } + + const auto authorization{request->getHeader("Authorization")}; + + if (authorization != m_authorization) { + response->setStatusCode(::drogon::k401Unauthorized); + response->setContentTypeCode(::drogon::CT_TEXT_PLAIN); + response->setBody("Unauthorized"); + + return std::nullopt; + } + + const std::string json_body{request->body()}; + + Json::CharReaderBuilder builder{}; + const auto reader{builder.newCharReader()}; + + std::string errors{}; + Json::Value root{}; + + if (!reader->parse(json_body.c_str(), json_body.c_str() + json_body.size(), &root, &errors)) { + response->setStatusCode(::drogon::k400BadRequest); + response->setContentTypeCode(::drogon::CT_TEXT_PLAIN); + response->setBody("Invalid webhook::json body"); + + return std::nullopt; + } + + response->setStatusCode(::drogon::k204NoContent); + + return root; +} \ No newline at end of file diff --git a/src/webhooks/models.cpp b/src/webhooks/models.cpp new file mode 100644 index 0000000..9124539 --- /dev/null +++ b/src/webhooks/models.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include + +using namespace topgg::webhook; + +static std::unordered_map parse_query_string(const std::string& query) { + std::unordered_map output{}; + + std::istringstream ss{query.substr(query.find('?') == 0 ? 1 : 0)}; + std::string pair{}; + + while (std::getline(ss, pair, '&')) { + const auto eq_pos{pair.find('=')}; + + if (eq_pos != std::string::npos) { + output[pair.substr(0, eq_pos)] = pair.substr(eq_pos + 1); + } + } + + return output; +} + +vote_event::vote_event(const json& j) { +#ifdef __TOPGG_DROGON_WEBHOOKS__ + receiver_id = j["bot"].asString(); + + if (receiver_id.empty()) { + receiver_id = j["guild"].asString(); + } + + voter_id = j["user"].asString(); + is_test = j["type"].asString() == "test"; + is_weekend = j.get("isWeekend", false).asBool(); + + const auto query_string{j["query"].asString()}; + + query = parse_query_string(query_string); +#else + try { + receiver_id = j["bot"].template get(); + } catch (TOPGG_UNUSED const std::exception&) { + receiver_id = j["guild"].template get(); + } + + voter_id = j["user"].template get(); + + try { + const auto type{j["type"].template get()}; + + is_test = type == "test"; + } catch (TOPGG_UNUSED const std::exception&) { + is_test = false; + } + + try { + is_weekend = j["isWeekend"].template get(); + } catch (TOPGG_UNUSED const std::exception&) { + is_weekend = false; + } + + try { + const auto query_string{j["query"].template get()}; + + query = parse_query_string(query_string); + } catch (TOPGG_UNUSED const std::exception&) {} +#endif +} \ No newline at end of file