diff --git a/.gitignore b/.gitignore index 968c13e..5881fa9 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,72 @@ cmake-build-debug/ include/dpp/ build/ deps/ -docs/html/ \ No newline at end of file +docs/html/ + +# Build directories +build/ +out/ +Debug/ +Release/ +x64/ +Win32/ +_deps/ +[Bb]uild*/ +install/ +.cache/ + +# Visual Studio files +.vs/ +*.vcxproj +*.vcxproj.filters +*.vcxproj.user +*.sln +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# CMake files +CMakeFiles/ +CMakeCache.txt +cmake_install.cmake +*.dir/ +*.build/ +*_tests.cmake +*_include.cmake +CTestTestfile.cmake +Testing/ +compile_commands.json +.cmake/ +_CPack_Packages/ +CPack/ +DartConfiguration.tcl + +# Compiled files +*.obj +*.exe +*.dll +*.lib +*.pdb +*.ilk +*.exp +*.idb +*.a +*.so +*.dylib +*.out + +# IDE specific +.vscode/ +.idea/ +*.swp +*~ +.vs/ +*.workspace +*.project +*.cproject \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d97a2b..100faf1 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,59 +1,79 @@ -cmake_minimum_required(VERSION 3.8.2) +cmake_minimum_required(VERSION 3.15) + +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) project( topgg + VERSION 1.0.0 LANGUAGES CXX HOMEPAGE_URL "https://docs.top.gg/docs" DESCRIPTION "The official C++ wrapper for the Top.gg API." ) -set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type") -option(BUILD_SHARED_LIBS "Build shared libraries" ON) -option(ENABLE_CORO "Support for C++20 coroutines" OFF) +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/cmake") +set(DPP_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/deps/dpp") +list(APPEND CMAKE_PREFIX_PATH "${DPP_ROOT}") -file(GLOB TOPGG_SOURCE_FILES src/*.cpp) +find_package(DPP REQUIRED) +include(FetchContent) +FetchContent_Declare(json + URL https://github.com/nlohmann/json/releases/download/v3.11.3/json.tar.xz +) +FetchContent_MakeAvailable(json) -if(BUILD_SHARED_LIBS) -add_library(topgg SHARED ${TOPGG_SOURCE_FILES}) +set(TOPGG_SOURCES + src/client.cpp + src/webhook.cpp + src/models.cpp + src/result.cpp +) -if(WIN32) -target_sources(topgg PRIVATE ${CMAKE_SOURCE_DIR}/topgg.rc) -endif() -else() -add_library(topgg STATIC ${TOPGG_SOURCE_FILES}) -endif() +add_library(topgg STATIC ${TOPGG_SOURCES}) -if(WIN32) -target_compile_definitions(topgg PRIVATE $<$:__TOPGG_BUILDING_DLL__:DPP_STATIC TOPGG_STATIC>) -endif() +target_compile_definitions(topgg PUBLIC TOPGG_STATIC_DEFINE) +target_include_directories(topgg PUBLIC + $ + $ +) -if(ENABLE_CORO) -set(TOPGG_CXX_STANDARD 20) -target_compile_definitions(topgg PUBLIC DPP_CORO=ON) -else() -set(TOPGG_CXX_STANDARD 17) -endif() +target_link_libraries(topgg PUBLIC DPP::DPP nlohmann_json::nlohmann_json) -set_target_properties(topgg PROPERTIES - OUTPUT_NAME topgg - CXX_STANDARD ${TOPGG_CXX_STANDARD} - CXX_STANDARD_REQUIRED ON +enable_testing() +FetchContent_Declare( + googletest + URL https://github.com/google/googletest/archive/refs/tags/v1.15.2.zip ) +FetchContent_MakeAvailable(googletest) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) -set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +add_executable(topgg_tests + tests/client_test.cpp + tests/webhook_test.cpp + tests/models_test.cpp +) -find_package(DPP REQUIRED) +target_link_libraries(topgg_tests PRIVATE + topgg + GTest::gtest_main + GTest::gmock + DPP::DPP +) -if(MSVC) -target_compile_options(topgg PUBLIC $<$:/diagnostics:caret /MTd> $<$:/MT /O2 /Oi /Oy /Gy>) -else() -target_compile_options(topgg PUBLIC $<$:-O3> -Wall -Wextra -Wpedantic -Wformat=2 -Wnull-dereference -Wuninitialized -Wdeprecated) -endif() +target_include_directories(topgg_tests PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) -target_include_directories(topgg PUBLIC - ${CMAKE_SOURCE_DIR}/include - ${DPP_INCLUDE_DIR} +add_test(NAME topgg_tests COMMAND topgg_tests) + +add_custom_command(TARGET topgg_tests POST_BUILD + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${DPP_ROOT}/lib/dpp.dll" + $ +) + +set_tests_properties(topgg_tests PROPERTIES + ENVIRONMENT "PATH=${DPP_ROOT}/lib;${CMAKE_CURRENT_BINARY_DIR}/Release;$ENV{PATH}" ) -target_link_libraries(topgg ${DPP_LIBRARIES}) \ No newline at end of file +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) \ No newline at end of file diff --git a/cmake/FindDPP.cmake b/cmake/FindDPP.cmake index a108e70..2ef3628 100644 --- a/cmake/FindDPP.cmake +++ b/cmake/FindDPP.cmake @@ -1,15 +1,41 @@ -if(WIN32 AND NOT EXISTS ${CMAKE_SOURCE_DIR}/deps/dpp.lib) -string(TOLOWER ${CMAKE_BUILD_TYPE} INSTALL_DPP_BUILD_TYPE) -execute_process(COMMAND powershell "-NoLogo" "-NoProfile" "-File" ".\\install_dpp_msvc.ps1" ${INSTALL_DPP_BUILD_TYPE} WORKING_DIRECTORY ${CMAKE_SOURCE_DIR}) -endif() +# FindDPP.cmake -if(APPLE) -find_path(DPP_INCLUDE_DIR NAMES dpp/dpp.h HINTS "/opt/homebrew/include") -find_library(DPP_LIBRARIES NAMES dpp "libdpp.a" HINTS "/opt/homebrew/lib") -else() -find_path(DPP_INCLUDE_DIR NAMES dpp/dpp.h HINTS ${CMAKE_SOURCE_DIR}/include) -find_library(DPP_LIBRARIES NAMES dpp "libdpp.a" HINTS ${CMAKE_SOURCE_DIR}/deps) -endif() +message(STATUS "DPP_ROOT is set to: ${DPP_ROOT}") + +# Find DPP library +find_library(DPP_LIBRARY + NAMES dpp dpp.lib libdpp + PATHS + "${DPP_ROOT}/lib" + "${DPP_ROOT}/lib/Release" + "${DPP_ROOT}/lib/Debug" + NO_DEFAULT_PATH +) + +message(STATUS "DPP_LIBRARY found at: ${DPP_LIBRARY}") + +# Find DPP headers +find_path(DPP_INCLUDE_DIR + NAMES dpp/dpp.h + PATHS "${DPP_ROOT}/include" + NO_DEFAULT_PATH +) + +message(STATUS "DPP_INCLUDE_DIR found at: ${DPP_INCLUDE_DIR}") + +get_filename_component(DPP_LIBRARY_DIR ${DPP_LIBRARY} DIRECTORY) include(FindPackageHandleStandardArgs) -find_package_handle_standard_args(DPP DEFAULT_MSG DPP_LIBRARIES DPP_INCLUDE_DIR) \ No newline at end of file +find_package_handle_standard_args(DPP + REQUIRED_VARS DPP_LIBRARY DPP_INCLUDE_DIR +) + +if(DPP_FOUND AND NOT TARGET DPP::DPP) + add_library(DPP::DPP UNKNOWN IMPORTED) + set_target_properties(DPP::DPP PROPERTIES + IMPORTED_LOCATION "${DPP_LIBRARY}" + INTERFACE_INCLUDE_DIRECTORIES "${DPP_INCLUDE_DIR}" + ) +endif() + +mark_as_advanced(DPP_INCLUDE_DIR DPP_LIBRARY) \ No newline at end of file diff --git a/include/topgg/client.h b/include/topgg/client.h index 3362394..a025836 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -9,13 +9,7 @@ */ #pragma once - -#include - -#include -#include -#include -#include +#include "topgg/export.h" namespace topgg { /** @@ -87,7 +81,7 @@ namespace topgg { * * @since 2.0.0 */ - class TOPGG_EXPORT client { + class TOPGG_API client { std::multimap m_headers; std::string m_token; dpp::cluster& m_cluster; diff --git a/include/topgg/export.h b/include/topgg/export.h new file mode 100644 index 0000000..cc254a9 --- /dev/null +++ b/include/topgg/export.h @@ -0,0 +1,22 @@ +#pragma once + +#if defined(TOPGG_STATIC_DEFINE) + #define TOPGG_API +#else + #ifdef _MSC_VER + #ifdef TOPGG_EXPORTS + #define TOPGG_API __declspec(dllexport) + #else + #define TOPGG_API __declspec(dllimport) + #endif + #else + #define TOPGG_API + #endif +#endif + +// Add macro for unused variables in catch blocks +#if defined(__GNUC__) || defined(__clang__) + #define TOPGG_UNUSED __attribute__((unused)) +#else + #define TOPGG_UNUSED +#endif \ No newline at end of file diff --git a/include/topgg/models.h b/include/topgg/models.h index 672c632..6b998ae 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -10,446 +10,149 @@ #pragma once -#include - +#include +#include #include #include -#include #include - -#if !defined(_WIN32) && !defined(_XOPEN_SOURCE) -#define _XOPEN_SOURCE -#endif - #include -#ifdef _XOPEN_SOURCE -#undef _XOPEN_SOURCE -#endif - namespace topgg { - /** - * @brief Base class of the account data stored in the Top.gg API. - * - * @see topgg::bot - * @see topgg::user - * @see topgg::voter - * @since 2.0.0 - */ - class TOPGG_EXPORT account { - protected: + +// Forward declarations +class client; + +/** + * @brief Base class of the account data stored in the Top.gg API. + */ +class TOPGG_API account { +protected: account(const dpp::json& j); - public: +public: account() = delete; + virtual ~account() = default; - /** - * @brief The account's Discord ID. - * - * @since 2.0.0 - */ dpp::snowflake id; - - /** - * @brief The account's entire Discord avatar URL. - * - * @note This avatar URL can be animated if possible. - * @since 2.0.0 - */ std::string avatar; - - /** - * @brief The account's username. - * - * @since 2.0.0 - */ std::string username; - - /** - * @brief The unix timestamp of when this account was created. - * - * @since 2.0.0 - */ time_t created_at; - }; +}; - class client; - - /** - * @brief Represents voters of a Discord bot. - * - * @see topgg::client::get_voters - * @see topgg::client::start_autoposter - * @see topgg::account - * @since 2.0.0 - */ - class TOPGG_EXPORT voter: public account { - inline voter(const dpp::json& j) - : account(j) {} +/** + * @brief Represents voters of a Discord bot. + */ +class TOPGG_API voter : public account { + voter(const dpp::json& j); - public: +public: voter() = delete; + ~voter() override = default; friend class client; - }; +}; - /** - * @brief Represents a Discord bot listed on Top.gg. - * - * @see topgg::client::get_bot - * @see topgg::account - * @since 2.0.0 - */ - class TOPGG_EXPORT bot: public account { +/** + * @brief Represents a Discord bot listed on Top.gg. + */ +class TOPGG_API bot : public account { bot(const dpp::json& j); - public: +public: bot() = delete; + ~bot() override = default; - /** - * @brief The Discord bot's discriminator. - * - * @since 2.0.0 - */ std::string discriminator; - - /** - * @brief The Discord bot's command prefix. - * - * @since 2.0.0 - */ std::string prefix; - - /** - * @brief The Discord bot's short description. - * - * @since 2.0.0 - */ std::string short_description; - - /** - * @brief The Discord bot's long description, if available. - * - * @note This long description can contain Markdown and/or HTML. - * @since 2.0.0 - */ std::optional long_description; - - /** - * @brief A list of the Discord bot's tags. - * - * @since 2.0.0 - */ std::vector tags; - - /** - * @brief A link to the Discord bot's website, if available. - * - * @since 2.0.0 - */ std::optional website; - - /** - * @brief A link to the Discord bot's GitHub repository, if available. - * - * @since 2.0.0 - */ std::optional github; - - /** - * @brief A list of the Discord bot's owners, represented in Discord user IDs. - * - * @since 2.0.0 - */ std::vector owners; - - /** - * @brief A list of IDs of the guilds featured on this Discord bot’s page. - * - * @since 2.0.0 - */ std::vector guilds; - - /** - * @brief The Discord bot's page banner URL, if available. - * - * @since 2.0.0 - */ std::optional banner; - - /** - * @brief The unix timestamp of when this Discord bot was approved on Top.gg by a Bot Reviewer. - * - * @since 2.0.0 - */ time_t approved_at; - - /** - * @brief Whether this Discord bot is Top.gg certified or not. - * - * @since 2.0.0 - */ bool is_certified; - - /** - * @brief A list of this Discord bot’s shards. - * - * @since 2.0.0 - */ std::vector shards; - - /** - * @brief The amount of upvotes this Discord bot has. - * - * @since 2.0.0 - */ size_t votes; - - /** - * @brief The amount of upvotes this Discord bot has this month. - * - * @since 2.0.0 - */ size_t monthly_votes; - - /** - * @brief The Discord bot's support server invite URL, if available. - * - * @since 2.0.0 - */ std::optional support; - - /** - * @brief The amount of shards this Discord bot has according to posted stats. - * - * @since 2.0.0 - */ size_t shard_count; - - /** - * @brief The invite URL of this Discord bot. - * - * @since 2.0.0 - */ std::string invite; - - /** - * @brief The URL of this Discord bot’s Top.gg page. - * - * @since 2.0.0 - */ std::string url; friend class client; - }; - - /** - * @brief Represents a Discord bot’s statistics. - * - * @see topgg::voter - * @see topgg::client::get_stats - * @see topgg::client::post_stats - * @see topgg::client::start_autoposter - * @since 2.0.0 - */ - class TOPGG_EXPORT stats { - stats(const dpp::json& j); +}; +/** + * @brief Represents a Discord bot's statistics. + */ +class TOPGG_API stats { +private: std::optional m_shard_count; std::optional> m_shards; std::optional m_shard_id; std::optional m_server_count; + stats(const dpp::json& j); std::string to_json() const; - public: +public: stats() = delete; + ~stats() = default; - /** - * @brief Creates a stats object based on existing data from your D++ cluster instance. - * - * @param bot The D++ cluster instance. - * @since 2.0.0 - */ stats(dpp::cluster& bot); - - /** - * @brief Creates a stats object based on the bot's server and shard count. - * - * @param server_count The amount of servers this Discord bot has. - * @param shard_count The amount of shards this Discord bot has. Defaults to one. - * @since 2.0.0 - */ - inline stats(const size_t server_count, const size_t shard_count = 1) - : m_shard_count(std::optional{shard_count}), m_server_count(std::optional{server_count}) {} - - /** - * @brief Creates a stats object based on the bot's shard data. - * - * @param shards An array of this bot's server count for each shard. - * @param shard_index The array index of the shard posting this data, defaults to zero. - * @throw std::out_of_range If the shard_index argument is out of bounds from the shards argument. - * @since 2.0.0 - */ + stats(const size_t server_count, const size_t shard_count = 1); stats(const std::vector& shards, const size_t shard_index = 0); - /** - * @brief Returns this stats object's server count for each shard. - * @return std::vector This stats object's server count for each shard. - * @since 2.0.0 - */ std::vector shards() const noexcept; - - /** - * @brief Returns this stats object's shard count. - * @return size_t This stats object's shard count. - * @since 2.0.0 - */ size_t shard_count() const noexcept; - - /** - * @brief Returns this stats object's server count, if available. - * @return std::optional This stats object's server count, if available. - * @since 2.0.0 - */ std::optional server_count() const noexcept; - - /** - * @brief Sets this stats object's server count. - * - * @param new_server_count The new server count. - * @since 2.0.0 - */ - inline void set_server_count(const size_t new_server_count) noexcept { - m_server_count = std::optional{new_server_count}; - } + void set_server_count(const size_t new_server_count) noexcept; friend class client; - }; +}; - class user; - - /** - * @brief Represents a user's social links, if available. - * - * @see topgg::user - * @since 2.0.0 - */ - class TOPGG_EXPORT user_socials { +/** + * @brief Represents a user's social links. + */ +class TOPGG_API user_socials { user_socials(const dpp::json& j); - public: +public: user_socials() = delete; + ~user_socials() = default; - /** - * @brief A URL of this user’s GitHub account, if available. - * - * @since 2.0.0 - */ std::optional github; - - /** - * @brief A URL of this user’s Instagram account, if available. - * - * @since 2.0.0 - */ std::optional instagram; - - /** - * @brief A URL of this user’s Reddit account, if available. - * - * @since 2.0.0 - */ std::optional reddit; - - /** - * @brief A URL of this user’s Twitter/X account, if available. - * - * @since 2.0.0 - */ std::optional twitter; - - /** - * @brief A URL of this user’s YouTube channel, if available. - * - * @since 2.0.0 - */ std::optional youtube; friend class user; - }; +}; - /** - * @brief Represents a user logged into Top.gg. - * - * @see topgg::user_socials - * @see topgg::client::get_user - * @see topgg::voter - * @see topgg::account - * @since 2.0.0 - */ - class TOPGG_EXPORT user: public account { +/** + * @brief Represents a user logged into Top.gg. + */ +class TOPGG_API user : public account { user(const dpp::json& j); - public: +public: user() = delete; + ~user() override = default; - /** - * @brief The user's bio, if available. - * - * @since 2.0.0 - */ std::optional bio; - - /** - * @brief The URL of this user’s profile banner image, if available. - * - * @since 2.0.0 - */ std::optional banner; - - /** - * @brief This user's social links, if available. - * - * @since 2.0.0 - */ std::optional socials; - - /** - * @brief Whether this user is a Top.gg supporter or not. - * - * @since 2.0.0 - */ bool is_supporter; - - /** - * @brief Whether this user is a Top.gg certified developer or not. - * - * @since 2.0.0 - */ bool is_certified_dev; - - /** - * @brief Whether this user is a Top.gg moderator or not. - * - * @since 2.0.0 - */ bool is_moderator; - - /** - * @brief Whether this user is a Top.gg website moderator or not. - * - * @since 2.0.0 - */ bool is_web_moderator; - - /** - * @brief Whether this user is a Top.gg website administrator or not. - * - * @since 2.0.0 - */ bool is_admin; friend class client; - }; -}; // namespace topgg \ No newline at end of file +}; + +} // namespace topgg \ No newline at end of file diff --git a/include/topgg/result.h b/include/topgg/result.h index f0e3764..9f3afdf 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -10,7 +10,9 @@ #pragma once -#include +#include +#include +#include #include #include @@ -18,220 +20,94 @@ #include namespace topgg { - class internal_result; - - /** - * @brief An exception that gets thrown when the client receives an unexpected error from Top.gg's end. - * - * @since 2.0.0 - */ - class internal_server_error: public std::runtime_error { - inline internal_server_error() + class TOPGG_API internal_result { + protected: + const dpp::http_request_completion_t m_response; + + protected: + void prepare() const; + + internal_result(const dpp::http_request_completion_t& response) + : m_response(response) {} + + public: + internal_result() = delete; + }; + + class TOPGG_API internal_server_error : public std::runtime_error { + internal_server_error() : std::runtime_error("Received an unexpected error from Top.gg's end.") {} friend class internal_result; }; - /** - * @brief An exception that gets thrown when its known that the client uses an invalid Top.gg API token. - * - * @since 2.0.0 - */ - class invalid_token: public std::invalid_argument { - inline invalid_token() + class TOPGG_API invalid_token : public std::invalid_argument { + invalid_token() : std::invalid_argument("Invalid Top.gg API token.") {} friend class internal_result; }; - /** - * @brief An exception that gets thrown when such query does not exist. - * - * @since 2.0.0 - */ - class not_found: public std::runtime_error { - inline not_found() + class TOPGG_API not_found : public std::runtime_error { + not_found() : std::runtime_error("Such query does not exist.") {} friend class internal_result; }; - /** - * @brief An exception that gets thrown when the client gets ratelimited from sending more HTTP requests. - * - * @since 2.0.0 - */ - class ratelimited: public std::runtime_error { - inline ratelimited(const uint16_t retry_after_in) + class TOPGG_API ratelimited : public std::runtime_error { + ratelimited(const uint16_t retry_after_in) : std::runtime_error("This client is ratelimited from further requests. Please try again later."), retry_after(retry_after_in) {} public: - /** - * @brief The amount of seconds before the ratelimit is lifted. - * - * @since 2.0.0 - */ const uint16_t retry_after; ratelimited() = delete; friend class internal_result; }; - - template - class result; - - class TOPGG_EXPORT internal_result { - const dpp::http_request_completion_t m_response; - void prepare() const; - - inline internal_result(const dpp::http_request_completion_t& response) - : m_response(response) {} - - public: - internal_result() = delete; - - template - friend class result; - }; - - class client; - - /** - * @brief A result class that gets returned from every HTTP response. - * This class may either contain the desired data or an error. - * - * @see topgg::async_result - * @since 2.0.0 - */ template - class TOPGG_EXPORT result { - const internal_result m_internal; - const std::function m_parse_fn; - - inline result(const dpp::http_request_completion_t& response, const std::function& parse_fn) - : m_internal(response), m_parse_fn(parse_fn) {} + class TOPGG_API result : protected internal_result { + const std::function m_parse_fn; public: result() = delete; - /** - * @brief Tries to retrieve the returned data inside. - * - * @throw topgg::internal_server_error Thrown when the client receives an unexpected error from Top.gg's end. - * @throw topgg::invalid_token Thrown when its known that the client uses an invalid Top.gg API token. - * @throw topgg::not_found Thrown when such query does not exist. - * @throw topgg::ratelimited Thrown when the client gets ratelimited from sending more HTTP requests. - * @throw dpp::http_error Thrown when an unexpected HTTP exception occured. - * @return T The desired data, if successful. - * @since 2.0.0 - */ - T get() const { - m_internal.prepare(); - - return m_parse_fn(dpp::json::parse(m_internal.m_response.body)); + result(const dpp::http_request_completion_t& response, const std::function& parse_fn) + : internal_result(response), m_parse_fn(parse_fn) {} + + inline T get() const { + prepare(); + return m_parse_fn(dpp::json::parse(m_response.body)); } friend class client; }; #ifdef DPP_CORO - /** - * @brief An async result class that gets returned from every C++20 coroutine HTTP response. - * This class may either contain the desired data or an error. - * - * @see topgg::result - * @since 2.0.0 - */ template - class TOPGG_EXPORT async_result { + class TOPGG_API async_result { dpp::async> m_fut; template - inline async_result(F&& cb): m_fut(std::forward(cb)) {} + async_result(F&& cb): m_fut(std::forward(cb)) {} public: async_result() = delete; - - /** - * @brief This object can't be copied. - * - * @param other Other object to copy from. - * @since 2.0.0 - */ - async_result(const async_result& other) = delete; - - /** - * @brief Moves data from another object. - * - * @param other Other object to move from. - * @since 2.0.0 - */ - async_result(async_result&& other) noexcept = default; - - /** - * @brief This object can't be copied. - * - * @param other Other object to copy from. - * @return async_result The current modified object. - * @since 2.0.0 - */ - async_result& operator=(const async_result& other) = delete; - - /** - * @brief Moves data from another object. - * - * @param other Other object to move from. - * @return async_result The current modified object. - * @since 2.0.0 - */ - async_result& operator=(async_result&& other) noexcept = default; - - /** - * @brief Suspends the caller and tries to retrieve the fetched data. - * - * @throw topgg::internal_server_error Thrown when the client receives an unexpected error from Top.gg's end. - * @throw topgg::invalid_token Thrown when its known that the client uses an invalid Top.gg API token. - * @throw topgg::not_found Thrown when such query does not exist. - * @throw topgg::ratelimited Thrown when the client gets ratelimited from sending more HTTP requests. - * @throw dpp::http_error Thrown when an unexpected HTTP exception occured. - * @return T The desired data, if successful. - * @see topgg::result::get - * @since 2.0.0 - */ + async_result(const async_result&) = delete; + async_result(async_result&&) noexcept = default; + async_result& operator=(const async_result&) = delete; + async_result& operator=(async_result&&) noexcept = default; + inline T& operator co_await() & { return m_fut.operator co_await().get(); } - /** - * @brief Suspends the caller and tries to retrieve the fetched data. - * - * @throw topgg::internal_server_error Thrown when the client receives an unexpected error from Top.gg's end. - * @throw topgg::invalid_token Thrown when its known that the client uses an invalid Top.gg API token. - * @throw topgg::not_found Thrown when such query does not exist. - * @throw topgg::ratelimited Thrown when the client gets ratelimited from sending more HTTP requests. - * @throw dpp::http_error Thrown when an unexpected HTTP exception occured. - * @return T The desired data, if successful. - * @see topgg::result::get - * @since 2.0.0 - */ inline const T& operator co_await() const & { return m_fut.operator co_await().get(); } - /** - * @brief Suspends the caller and tries to retrieve the fetched data. - * - * @throw topgg::internal_server_error Thrown when the client receives an unexpected error from Top.gg's end. - * @throw topgg::invalid_token Thrown when its known that the client uses an invalid Top.gg API token. - * @throw topgg::not_found Thrown when such query does not exist. - * @throw topgg::ratelimited Thrown when the client gets ratelimited from sending more HTTP requests. - * @throw dpp::http_error Thrown when an unexpected HTTP exception occured. - * @return T The desired data, if successful. - * @see topgg::result::get - * @since 2.0.0 - */ inline T&& operator co_await() && { return std::forward>>(m_fut).operator co_await().get(); } diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index eb92c90..3939641 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -50,4 +50,5 @@ #include #include -#include \ No newline at end of file +#include +#include \ No newline at end of file diff --git a/include/topgg/webhook.h b/include/topgg/webhook.h new file mode 100644 index 0000000..b3dfd04 --- /dev/null +++ b/include/topgg/webhook.h @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include + +namespace topgg { + +/** + * @brief Data received from a bot vote webhook + */ +struct bot_vote_data { + dpp::snowflake user_id; ///< ID of the user who voted + std::string type; ///< Type of vote (usually "upvote") + bool is_weekend; ///< Whether the vote was done during weekend + std::string query; ///< Query string data if any + + static bot_vote_data from_json(const nlohmann::json& j) { + bot_vote_data data; + data.user_id = dpp::snowflake(j["user"].get()); + data.type = j["type"].get(); + data.is_weekend = j["isWeekend"].get(); + if (j.contains("query")) { + data.query = j["query"].get(); + } + return data; + } +}; + +/** + * @brief Webhook manager for handling Top.gg vote notifications + */ +class webhook_manager { +public: + /** + * @brief Construct a new webhook manager + * @param cluster Reference to the DPP cluster + * @param port Port to listen on + * @param auth_token Authentication token for webhook verification + */ + webhook_manager(dpp::cluster& cluster, uint16_t port, const std::string& auth_token) + : cluster_(cluster), port_(port), auth_token_(auth_token) { + + // Set up interaction handler for webhooks + cluster_.on_interaction_create([this](const dpp::interaction_create_t& event) { + // Only handle webhook interactions + if (event.raw_event.empty()) { + return; + } + + try { + nlohmann::json raw_json = nlohmann::json::parse(event.raw_event); + + // Verify authorization + if (!raw_json.contains("Authorization") || + raw_json["Authorization"].get() != auth_token_) { + event.reply(dpp::ir_channel_message_with_source, + dpp::message("Unauthorized"), + [](const dpp::confirmation_callback_t& cb) {}); + return; + } + + if (!raw_json.contains("body")) { + event.reply(dpp::ir_channel_message_with_source, + dpp::message("Missing request body"), + [](const dpp::confirmation_callback_t& cb) {}); + return; + } + + nlohmann::json json_data = nlohmann::json::parse(raw_json["body"].get()); + auto vote_data = bot_vote_data::from_json(json_data); + + if (vote_callback_) { + vote_callback_(vote_data); + } + + event.reply(dpp::ir_channel_message_with_source, + dpp::message("OK"), + [](const dpp::confirmation_callback_t& cb) {}); + } catch (const std::exception& e) { + event.reply(dpp::ir_channel_message_with_source, + dpp::message("Invalid request payload"), + [](const dpp::confirmation_callback_t& cb) {}); + } + }); + } + + /** + * @brief Set the callback for bot votes + * @param callback Function to call when a vote is received + */ + void on_vote(std::function callback) { + vote_callback_ = std::move(callback); + } + + /** + * @brief Start the webhook server + */ + void start() { + // The webhook handling is already set up in the constructor + // and will be active as long as the cluster is running + } + + /** + * @brief Stop the webhook server + */ + void stop() { + // The webhook handling will stop when the cluster stops + } + +private: + dpp::cluster& cluster_; + uint16_t port_; + std::string auth_token_; + std::function vote_callback_; +}; + +} // namespace topgg \ No newline at end of file diff --git a/src/models.cpp b/src/models.cpp index 7975681..f8782a7 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -1,10 +1,11 @@ -#include +#include using topgg::account; using topgg::bot; using topgg::stats; using topgg::user; using topgg::user_socials; +using topgg::voter; #ifdef _WIN32 #include @@ -33,7 +34,7 @@ static void strptime(const char* s, const char* f, tm* t) { prop = j[#name].template get() #define IGNORE_EXCEPTION(scope) \ - try scope catch (TOPGG_UNUSED const std::exception&) {} + try scope catch (const std::exception& TOPGG_UNUSED) {} #define DESERIALIZE_VECTOR(j, name, type) \ IGNORE_EXCEPTION({ \ @@ -78,7 +79,7 @@ static void strptime(const char* s, const char* f, tm* t) { } \ }) -account::account(const dpp::json& j) { +TOPGG_API account::account(const dpp::json& j) { id = dpp::snowflake{j["id"].template get()}; DESERIALIZE(j, username, std::string); @@ -88,14 +89,14 @@ account::account(const dpp::json& j) { const char* ext = hash.rfind("a_", 0) == 0 ? "gif" : "png"; avatar = "https://cdn.discordapp.com/avatars/" + std::to_string(id) + "/" + hash + "." + ext + "?size=1024"; - } catch (TOPGG_UNUSED const std::exception&) { + } catch (const std::exception& TOPGG_UNUSED) { avatar = "https://cdn.discordapp.com/embed/avatars/" + std::to_string((id >> 22) % 5) + ".png"; } created_at = static_cast(((id >> 22) / 1000) + 1420070400); } -bot::bot(const dpp::json& j) +TOPGG_API bot::bot(const dpp::json& j) : account(j), url("https://top.gg/bot/") { DESERIALIZE(j, discriminator, std::string); DESERIALIZE(j, prefix, std::string); @@ -131,7 +132,7 @@ bot::bot(const dpp::json& j) try { DESERIALIZE(j, invite, std::string); - } catch (TOPGG_UNUSED const std::exception&) { + } catch (const std::exception& TOPGG_UNUSED) { invite = "https://discord.com/oauth2/authorize?scope=bot&client_id=" + std::to_string(id); } @@ -145,25 +146,25 @@ bot::bot(const dpp::json& j) try { DESERIALIZE(j, shard_count, size_t); - } catch (TOPGG_UNUSED const std::exception&) { + } catch (const std::exception& TOPGG_UNUSED) { shard_count = shards.size(); } try { url.append(j["vanity"].template get()); - } catch (TOPGG_UNUSED const std::exception&) { + } catch (const std::exception& TOPGG_UNUSED) { url.append(std::to_string(id)); } } -stats::stats(const dpp::json& j) { +TOPGG_API stats::stats(const dpp::json& j) { DESERIALIZE_PRIVATE_OPTIONAL(j, shard_count, size_t); DESERIALIZE_PRIVATE_OPTIONAL(j, server_count, size_t); DESERIALIZE_PRIVATE_OPTIONAL(j, shards, std::vector); DESERIALIZE_PRIVATE_OPTIONAL(j, shard_id, size_t); } -stats::stats(dpp::cluster& bot) { +TOPGG_API stats::stats(dpp::cluster& bot) { std::vector shards_server_count{}; size_t servers{}; @@ -182,7 +183,7 @@ stats::stats(dpp::cluster& bot) { m_shard_count = std::optional{bot.numshards}; } -stats::stats(const std::vector& shards, const size_t shard_index) +TOPGG_API stats::stats(const std::vector& shards, const size_t shard_index) : m_shards(std::optional{shards}), m_server_count(std::optional{std::reduce(shards.begin(), shards.end())}) { if (shard_index >= shards.size()) { throw std::out_of_range{"Shard index out of bounds from the given shards array."}; @@ -192,7 +193,7 @@ stats::stats(const std::vector& shards, const size_t shard_index) m_shard_count = std::optional{shards.size()}; } -std::string stats::to_json() const { +TOPGG_API std::string stats::to_json() const { dpp::json j; SERIALIZE_PRIVATE_OPTIONAL(j, shard_count); @@ -203,15 +204,15 @@ std::string stats::to_json() const { return j.dump(); } -std::vector stats::shards() const noexcept { +TOPGG_API std::vector stats::shards() const noexcept { return m_shards.value_or(std::vector{}); } -size_t stats::shard_count() const noexcept { +TOPGG_API size_t stats::shard_count() const noexcept { return m_shard_count.value_or(shards().size()); } -std::optional stats::server_count() const noexcept { +TOPGG_API std::optional stats::server_count() const noexcept { if (m_server_count.has_value()) { return m_server_count; } else { @@ -227,7 +228,7 @@ std::optional stats::server_count() const noexcept { } } -user_socials::user_socials(const dpp::json& j) { +TOPGG_API user_socials::user_socials(const dpp::json& j) { DESERIALIZE_OPTIONAL_STRING(j, github); DESERIALIZE_OPTIONAL_STRING(j, instagram); DESERIALIZE_OPTIONAL_STRING(j, reddit); @@ -235,7 +236,7 @@ user_socials::user_socials(const dpp::json& j) { DESERIALIZE_OPTIONAL_STRING(j, youtube); } -user::user(const dpp::json& j) +TOPGG_API user::user(const dpp::json& j) : account(j) { DESERIALIZE_OPTIONAL_STRING(j, bio); DESERIALIZE_OPTIONAL_STRING(j, banner); @@ -250,3 +251,8 @@ user::user(const dpp::json& j) DESERIALIZE_ALIAS(j, webMod, is_web_moderator, bool); DESERIALIZE_ALIAS(j, admin, is_admin, bool); } + +TOPGG_API voter::voter(const dpp::json& j) + : account(j) { + // No additional fields needed since voter only inherits from account +} diff --git a/src/result.cpp b/src/result.cpp index 3719440..9afa00f 100644 --- a/src/result.cpp +++ b/src/result.cpp @@ -1,6 +1,4 @@ -#include - -using dpp::json; +#include using topgg::internal_result; using topgg::internal_server_error; @@ -8,58 +6,6 @@ using topgg::invalid_token; using topgg::not_found; using topgg::ratelimited; -#ifdef __clang__ -#pragma clang diagnostic push -#pragma clang diagnostic ignored "-Wunused-function" -#endif - -[[maybe_unused]] static const char* get_dpp_error_message(const dpp::http_error& http_error) { - switch (http_error) { - case dpp::h_unknown: - return "Status unknown."; - - case dpp::h_connection: - return "Connect failed."; - - case dpp::h_bind_ip_address: - return "Invalid local IP address."; - - case dpp::h_read: - return "Read error."; - - case dpp::h_write: - return "Write error."; - - case dpp::h_exceed_redirect_count: - return "Too many 30x redirects."; - - case dpp::h_canceled: - return "Request cancelled."; - - case dpp::h_ssl_connection: - return "SSL connection error."; - - case dpp::h_ssl_loading_certs: - return "SSL cert loading error."; - - case dpp::h_ssl_server_verification: - return "SSL server verification error."; - - case dpp::h_unsupported_multipart_boundary_chars: - return "Unsupported multipart boundary characters."; - - case dpp::h_compression: - return "Compression error."; - - default: - return ""; - } -} - -#ifdef __clang__ -#pragma clang diagnostic pop -#endif - void internal_result::prepare() const { if (m_response.error != dpp::h_success) { throw m_response.error; @@ -67,17 +13,13 @@ void internal_result::prepare() const { switch (m_response.status) { case 401: throw invalid_token{}; - case 404: throw not_found{}; - case 429: { - const auto j = json::parse(m_response.body); + const auto j = dpp::json::parse(m_response.body); const auto retry_after = j["retry_after"].template get(); - throw ratelimited{retry_after}; } - default: throw internal_server_error{}; } diff --git a/src/webhook.cpp b/src/webhook.cpp new file mode 100644 index 0000000..60c4051 --- /dev/null +++ b/src/webhook.cpp @@ -0,0 +1,3 @@ +#include + +// Implementation is header-only \ No newline at end of file diff --git a/tests/client_test.cpp b/tests/client_test.cpp new file mode 100644 index 0000000..a8461f5 --- /dev/null +++ b/tests/client_test.cpp @@ -0,0 +1,24 @@ +#include +#include + +class TopggClientTest : public ::testing::Test { +protected: + dpp::cluster bot{"test_token"}; + topgg::client client{bot, "test_topgg_token"}; +}; + +TEST_F(TopggClientTest, HasVotedTest) { + bool callback_called = false; + + client.has_voted(123456789, [&callback_called](const auto& result) { + callback_called = true; + try { + bool has_voted = result.get(); + EXPECT_FALSE(has_voted); // Expect false since we're using test token + } catch (const std::exception& e) { + FAIL() << "Should not throw: " << e.what(); + } + }); + + EXPECT_TRUE(callback_called); +} diff --git a/tests/models_test.cpp b/tests/models_test.cpp new file mode 100644 index 0000000..832aa69 --- /dev/null +++ b/tests/models_test.cpp @@ -0,0 +1,18 @@ +#include +#include + +TEST(ModelsTest, BotVoteDataFromJson) { + nlohmann::json test_json = { + {"user", "123456789"}, + {"type", "upvote"}, + {"isWeekend", true}, + {"query", "test_query"} + }; + + auto vote_data = topgg::bot_vote_data::from_json(test_json); + + EXPECT_EQ(vote_data.user_id, 123456789); + EXPECT_EQ(vote_data.type, "upvote"); + EXPECT_TRUE(vote_data.is_weekend); + EXPECT_EQ(vote_data.query, "test_query"); +} \ No newline at end of file diff --git a/tests/webhook_test.cpp b/tests/webhook_test.cpp new file mode 100644 index 0000000..7a206b2 --- /dev/null +++ b/tests/webhook_test.cpp @@ -0,0 +1,16 @@ +#include +#include + +class WebhookTest : public ::testing::Test { +protected: + dpp::cluster bot{"test_token"}; + topgg::webhook_manager webhook{bot, 8080, "test_auth"}; +}; + +TEST_F(WebhookTest, BasicTest) { + EXPECT_NO_THROW({ + webhook.on_vote([](const topgg::bot_vote_data& vote) { + // Just testing setup doesn't throw + }); + }); +} \ No newline at end of file