diff --git a/.gitattributes b/.gitattributes index c80a136..04a9612 100644 --- a/.gitattributes +++ b/.gitattributes @@ -6,7 +6,6 @@ README.md eol=lf CMakeLists.txt eol=lf docs/Doxyfile eol=lf docs/*.png binary -docs/*.css eol=lf linguist-vendored=true docs/*.html eol=lf linguist-vendored=true **/*.cmake eol=lf **/*.cpp eol=lf diff --git a/.gitignore b/.gitignore index 968c13e..f5e1998 100644 --- a/.gitignore +++ b/.gitignore @@ -1,10 +1,25 @@ **/*.deb **/*.rpm +**/*.exe +**/*.obj +*.cmake .idea + cmake-build-debug/ +CMakePresets.json .vscode/ include/dpp/ -build/ -deps/ -docs/html/ \ No newline at end of file +include/cpp-httplib/ +include/drogon/ +include/nlohmann/ +**/build/ +**/*.dll +**/*.lib +docs/html/ + +cpp-httplib/ +json/ + +conan_toolchain.cmake +*conan*.bat \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d97a2b..004823b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,30 +1,38 @@ -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 community-maintained C++ API Client for the Top.gg API." ) -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_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(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 +42,32 @@ 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() + +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} ) -target_link_libraries(topgg ${DPP_LIBRARIES}) \ No newline at end of file +target_link_libraries(topgg PUBLIC ${DPP_LIBRARIES}) \ No newline at end of file diff --git a/LICENSE b/LICENSE index 1563e02..ffa658e 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 Top.gg & null8626 +Copyright (c) 2024-2025 Top.gg & null8626 Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/cmake/FindDPP.cmake b/cmake/FindDPP.cmake index a108e70..8f15eab 100644 --- a/cmake/FindDPP.cmake +++ b/cmake/FindDPP.cmake @@ -1,14 +1,16 @@ -if(WIN32 AND NOT EXISTS ${CMAKE_SOURCE_DIR}/deps/dpp.lib) +get_filename_component(CMAKE_CURRENT_LIST_DIRECTORY "${CMAKE_CURRENT_LIST_FILE}" PATH) + +if(WIN32 AND NOT EXISTS ${CMAKE_CURRENT_LIST_DIRECTORY}/../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}) +execute_process(COMMAND powershell "-NoLogo" "-NoProfile" "-File" "${CMAKE_CURRENT_LIST_DIRECTORY}/../install_dpp_msvc.ps1" ${INSTALL_DPP_BUILD_TYPE} WORKING_DIRECTORY "${CMAKE_CURRENT_LIST_DIRECTORY}/..") endif() 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) +find_path(DPP_INCLUDE_DIR NAMES dpp/dpp.h HINTS ${CMAKE_CURRENT_LIST_DIRECTORY}/../include) +find_library(DPP_LIBRARIES NAMES dpp "libdpp.a" HINTS ${CMAKE_CURRENT_LIST_DIRECTORY}/../deps) endif() include(FindPackageHandleStandardArgs) diff --git a/docs/Doxyfile b/docs/Doxyfile index b18a078..383390c 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -48,13 +48,13 @@ PROJECT_NAME = "Top.gg C++ SDK" # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2.0.0 +PROJECT_NUMBER = 2.1.0 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a # quick idea about the purpose of the project. Keep the description short. -PROJECT_BRIEF = "The official C++ wrapper for the Top.gg API." +PROJECT_BRIEF = "A community-maintained C++ API Client for the Top.gg API." # With the PROJECT_LOGO tag one can specify a logo or an icon that is included # in the documentation. The maximum height of the logo should not exceed 55 @@ -1348,8 +1348,7 @@ HTML_STYLESHEET = # This tag requires that the tag GENERATE_HTML is set to YES. HTML_EXTRA_STYLESHEET = doxygen-awesome-css/doxygen-awesome.css \ - doxygen-awesome-css/doxygen-awesome-sidebar-only.css \ - style.css + doxygen-awesome-css/doxygen-awesome-sidebar-only.css # The HTML_EXTRA_FILES tag can be used to specify one or more extra images or # other source files which should be copied to the HTML output directory. Note diff --git a/docs/header.html b/docs/header.html index 46947b2..381859d 100644 --- a/docs/header.html +++ b/docs/header.html @@ -5,8 +5,8 @@ - - + + diff --git a/docs/style.css b/docs/style.css deleted file mode 100644 index b426843..0000000 --- a/docs/style.css +++ /dev/null @@ -1,58 +0,0 @@ -html { - --font-family-monospace: 'Roboto Mono'; - --separator-color: rgba(0, 0, 0, 0); -} - -#projectname a, -div.header .title, -#projectname, -h1, -h2.groupheader { - font-weight: 900; -} - -a:hover { - cursor: pointer; - text-decoration: underline; -} - -#top { - border-bottom: none; -} - -div.contents { - margin: 0px auto var(--spacing-medium) auto; - padding-bottom: var(--spacing-large); -} - -@media screen and (min-width: 768px) { - html, - body { - position: relative; - height: 100%; - } - - #apis { - position: relative; - height: calc(100% - var(--top-height) + var(--spacing-large)); - } - - #top { - position: -webkit-sticky; - position: sticky; - top: 0px; - height: 100%; - } - - #doc-content { - position: relative; - top: calc(var(--top-height) - 100%); - padding-top: 0px; - height: 100% !important; - } -} - -#nav-path, -#nav-sync { - display: none; -} diff --git a/include/topgg/client.h b/include/topgg/client.h index 3362394..0eb4a05 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -1,11 +1,11 @@ /** * @module topgg * @file client.h - * @brief The official C++ wrapper for the Top.gg API. + * @brief A community-maintained C++ API Client for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-07-12 - * @version 2.0.0 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-10-02 + * @version 2.1.0 */ #pragma once @@ -65,7 +65,7 @@ namespace topgg { * @since 2.0.0 */ using is_weekend_completion_t = std::function&)>; - + /** * @brief The callback function to call when post_stats completes. * @@ -73,39 +73,47 @@ namespace topgg { * @since 2.0.0 */ using post_stats_completion_t = std::function; - + /** - * @brief The callback function that retrieves the bot's stats. + * @brief The callback function to call after every autopost request to the API, successful or not. * * @see topgg::client::start_autoposter + * @see topgg::client::stop_autoposter * @since 2.0.0 */ using custom_autopost_callback_t = std::function<::topgg::stats(dpp::cluster&)>; - + /** - * @brief Main client class that lets you make HTTP requests with the Top.gg API. + * @brief Interact with the API's endpoints. * * @since 2.0.0 */ class TOPGG_EXPORT client { - std::multimap m_headers; - std::string m_token; + dpp::http_headers m_headers; + dpp::snowflake m_id; dpp::cluster& m_cluster; dpp::timer m_autoposter_timer; - template - void basic_request(const std::string& url, const std::function&)>& callback, std::function&& conversion_fn) { - m_cluster.request("https://top.gg/api" + url, dpp::m_get, [callback, conversion_fn_in = std::move(conversion_fn)](const auto& response) { callback(result{response, conversion_fn_in}); }, "", "application/json", m_headers); + void request(const dpp::http_method method, const std::string& url, const dpp::http_completion_event callback, const std::string& body = "") { + m_cluster.request(TOPGG_BASE_URL + url, method, callback, body, "application/json", m_headers); } + template + void basic_request(const dpp::http_method method, const std::string& url, const std::function&)>& callback, std::function&& conversion_fn, const std::string& body = "") { + request(method, url, [callback, conversion_fn_in = std::move(conversion_fn)](const dpp::http_request_completion_t& response) { callback(result{response, conversion_fn_in}); }, body); + } + + stats get_stats(); + void post_stats_inner(const size_t server_count, const dpp::http_completion_event callback); + public: client() = delete; /** - * @brief Constructs the client class. + * @brief Creates a client object. * * @param cluster A pointer to the bot's D++ cluster using this library. - * @param token The Top.gg API token to use. + * @param token The API token to use. * @since 2.0.0 */ client(dpp::cluster& cluster, const std::string& token); @@ -145,26 +153,9 @@ namespace topgg { client& operator=(client&& other) = delete; /** - * @brief Fetches a listed Discord bot from a Discord ID. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * @brief Fetches a Discord bot from its ID. * - * topgg_client.get_bot(264811613708746752, [](const auto& result) { - * try { - * const auto topgg_bot = result.get(); - * - * std::cout << topgg_bot.username << std::endl; - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * }); - * ``` - * - * @param bot_id The Discord bot ID to fetch from. + * @param bot_id The requested ID. * @param callback The callback function to call when get_bot completes. * @note For its C++20 coroutine counterpart, see co_get_bot. * @see topgg::result @@ -176,29 +167,14 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches a listed Discord bot from a Discord ID through a C++20 coroutine. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * try { - * const auto topgg_bot = co_await topgg_client.co_get_bot(264811613708746752); - * - * std::cout << topgg_bot.username << std::endl; - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * ``` - * - * @param bot_id The Discord bot ID to fetch from. - * @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. + * @brief Fetches a Discord bot from its ID through a C++20 coroutine. + * + * @param bot_id The requested ID. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::not_found The specified bot does not exist. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. * @return co_await to retrieve a topgg::bot if successful * @note For its C++17 callback-based counterpart, see get_bot. * @see topgg::async_result @@ -210,25 +186,10 @@ namespace topgg { #endif /** + * @deprecated No longer supported by API v0. + * * @brief Fetches a user from a Discord ID. * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * topgg_client.get_user(661200758510977084, [](const auto& result) { - * try { - * const auto user = result.get(); - * - * std::cout << user.username << std::endl; - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * }); - * ``` - * * @param user_id The Discord user ID to fetch from. * @param callback The callback function to call when get_user completes. * @note For its C++20 coroutine counterpart, see co_get_user. @@ -241,23 +202,10 @@ namespace topgg { #ifdef DPP_CORO /** + * @deprecated No longer supported by API v0. + * * @brief Fetches a user from a Discord ID through a C++20 coroutine. * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * try { - * const auto user = co_await topgg_client.co_get_user(661200758510977084); - * - * std::cout << user.username << std::endl; - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * ``` - * * @param user_id The Discord user ID to fetch from. * @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. @@ -275,24 +223,18 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot’s statistics. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * @brief Returns an object that allows you to configure a bot query before sending it to the API. * - * topgg_client.get_stats([](const auto& result) { - * try { - * auto stats = result.get(); - * - * std::cout << stats.server_count().value_or(0) << std::endl; - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * }); - * ``` + * @return bot_query An object that allows you to configure a bot query before sending it to the API. + * @see topgg::bot_query + * @since 2.0.1 + */ + inline bot_query get_bots() noexcept { + return bot_query{this}; + } + + /** + * @brief Fetches your Discord bot's posted statistics. * * @param callback The callback function to call when get_stats completes. * @note For its C++20 coroutine counterpart, see co_get_stats. @@ -305,29 +247,13 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot’s statistics through a C++20 coroutine. - * - * Example: + * @brief Fetches your Discord bot's posted statistics through a C++20 coroutine. * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * try { - * const auto stats = co_await topgg_client.co_get_stats(); - * - * std::cout << stats.server_count().value_or(0) << std::endl; - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * ``` - * - * @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 co_await to retrieve a topgg::stats if successful + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. + * @return co_await to retrieve an optional size_t if successful * @note For its C++17 callback-based counterpart, see get_stats. * @see topgg::async_result * @see topgg::client::start_autoposter @@ -338,26 +264,22 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot’s last 1000 voters. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * @brief Fetches your project's recent 100 unique voters. * - * topgg_client.get_voters([](const auto& result) { - * try { - * auto voters = result.get(); - * - * for (auto& voter: voters) { - * std::cout << voter.username << std::endl; - * } - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * }); - * ``` + * @param page The page number. Each page can only have at most 100 voters. + * @param callback The callback function to call when get_voters completes. + * @note For its C++20 coroutine counterpart, see co_get_voters. + * @see topgg::result + * @see topgg::voter + * @see topgg::stats + * @see topgg::client::start_autoposter + * @see topgg::client::co_get_voters + * @since 2.0.0 + */ + void get_voters(size_t page, const get_voters_completion_t& callback); + + /** + * @brief Fetches your project's recent 100 unique voters. * * @param callback The callback function to call when get_voters completes. * @note For its C++20 coroutine counterpart, see co_get_voters. @@ -372,31 +294,14 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot’s last 1000 voters through a C++20 coroutine. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * try { - * const auto voters = co_await topgg_client.co_get_voters(); - * - * for (const auto& voter: voters) { - * std::cout << voter.username << std::endl; - * } - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * ``` - * - * @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 co_await to retrieve a std::vector if successful + * @brief Fetches your project's recent 100 unique voters through a C++20 coroutine. + * + * @param page The page number. Each page can only have at most 100 voters. Defaults to 1. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. + * @return co_await to retrieve a vector of topgg::voter if successful * @note For its C++17 callback-based counterpart, see get_voters. * @see topgg::async_result * @see topgg::voter @@ -405,67 +310,32 @@ namespace topgg { * @see topgg::client::get_voters * @since 2.0.0 */ - topgg::async_result> co_get_voters(); + topgg::async_result> co_get_voters(size_t page = 1); #endif /** - * @brief Checks if the specified user has voted your Discord bot. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * @brief Checks if the specified Top.gg user has voted for your project in the past 12 hours. * - * topgg_client.has_voted(661200758510977084, [](const auto& result) { - * try { - * if (result.get()) { - * std::cout << "checks out" << std::endl; - * } - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * }); - * ``` - * - * @param user_id The Discord user ID to check from. + * @param user_id The requested user's ID. * @param callback The callback function to call when has_voted completes. * @note For its C++20 coroutine counterpart, see co_has_voted. * @see topgg::result * @see topgg::stats * @see topgg::client::start_autoposter - * @note For its C++20 coroutine counterpart, see co_has_voted. * @since 2.0.0 */ void has_voted(const dpp::snowflake user_id, const has_voted_completion_t& callback); #ifdef DPP_CORO /** - * @brief Checks if the specified user has voted your Discord bot through a C++20 coroutine. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * try { - * const auto voted = co_await topgg_client.co_has_voted(661200758510977084); - * - * if (voted) { - * std::cout << "checks out" << std::endl; - * } - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * ``` - * - * @param user_id The Discord user ID to check from. - * @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. + * @brief Checks if the specified Top.gg user has voted for your project in the past 12 hours through a C++20 coroutine. + * + * @param user_id The requested user's ID. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::not_found The specified user has not logged in to Top.gg. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. * @return co_await to retrieve a bool if successful * @note For its C++17 callback-based counterpart, see has_voted. * @see topgg::async_result @@ -478,24 +348,7 @@ namespace topgg { #endif /** - * @brief Checks if the weekend multiplier is active. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * topgg_client.is_weekend([](const auto& result) { - * try { - * if (result.get()) { - * std::cout << "the weekend multiplier is active" << std::endl; - * } - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * }); - * ``` + * @brief Checks if the weekend multiplier is active, where a single vote counts as two. * * @param callback The callback function to call when is_weekend completes. * @note For its C++20 coroutine counterpart, see co_is_weekend. @@ -507,30 +360,12 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Checks if the weekend multiplier is active through a C++20 coroutine. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * try { - * const auto is_weekend = co_await topgg_client.co_is_weekend(); - * - * if (is_weekend) { - * std::cout << "the weekend multiplier is active" << std::endl; - * } - * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; - * } - * ``` + * @brief Checks if the weekend multiplier is active through a C++20 coroutine, where a single vote counts as two. * - * @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. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. * @return co_await to retrieve a bool if successful * @note For its C++17 callback-based counterpart, see is_weekend. * @see topgg::async_result @@ -541,20 +376,7 @@ namespace topgg { #endif /** - * @brief Manually posts your Discord bot's statistics using data directly from your D++ cluster instance. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * topgg_client.post_stats([](const auto success) { - * if (success) { - * std::cout << "stats posted!" << std::endl; - * } - * }); - * ``` + * @brief Posts your Discord bot's statistics to the API. This will update the statistics in your bot's Top.gg page. * * @param callback The callback function to call when post_stats completes. * @note For its C++20 coroutine counterpart, see co_post_stats. @@ -567,20 +389,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Manually posts your Discord bot's statistics using data directly from your D++ cluster instance through a C++20 coroutine. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * const auto success = co_await topgg_client.co_post_stats(); - * - * if (success) { - * std::cout << "stats posted!" << std::endl; - * } - * ``` + * @brief Posts your Discord bot's statistics to the API through a C++20 coroutine. This will update the statistics in your bot's Top.gg page. * * @return co_await to retrieve a bool * @note For its C++17 callback-based counterpart, see post_stats. @@ -592,134 +401,85 @@ namespace topgg { #endif /** - * @brief Manually posts your Discord bot's statistics. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * @brief Starts autoposting your Discord bot's statistics using data directly from your D++ cluster instance. * - * const size_t server_count = 12345; - * - * topgg_client.post_stats(topgg::stats{server_count}, [](const auto success) { - * if (success) { - * std::cout << "stats posted!" << std::endl; - * } - * }); - * ``` - * - * @param s Your Discord bot's statistics. - * @param callback The callback function to call when post_stats completes. - * @note For its C++20 coroutine counterpart, see co_post_stats. - * @see topgg::result - * @see topgg::stats - * @see topgg::client::start_autoposter - * @see topgg::client::co_post_stats + * @param callback The callback function to call after every request to the API, successful or not. + * @param interval The interval between posting in seconds. Defaults to 15 minutes. + * @note This function has no effect if the autoposter is already running. + * @see topgg::client::post_stats + * @see topgg::client::stop_autoposter + * @see topgg::custom_autopost_callback_t * @since 2.0.0 */ - void post_stats(const stats& s, const post_stats_completion_t& callback); + void start_autoposter(const post_stats_completion_t& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); -#ifdef DPP_CORO /** - * @brief Manually posts your Discord bot's statistics through a C++20 coroutine. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * @brief Starts autoposting your Discord bot's statistics using data directly from your D++ cluster instance. * - * const size_t server_count = 12345; - * const auto success = co_await topgg_client.co_post_stats(topgg::stats{server_count}); - * - * if (success) { - * std::cout << "stats posted!" << std::endl; - * } - * ``` + * @param stats_callback The callback function to call after every request to the API, successful or not. + * @param post_callback The callback function that retrieves your bot's statistics. + * @param interval The interval between posting in seconds. Defaults to 15 minutes. + * @note This function has no effect if the autoposter is already running. + * @see topgg::client::post_stats + * @see topgg::client::stop_autoposter + * @see topgg::custom_autopost_callback_t + * @since 2.1.0 + */ + void start_autoposter(const custom_autopost_callback_t& stats_callback, const post_stats_completion_t& post_callback, time_t interval); + + /** + * @brief Starts autoposting your Discord bot's statistics using data directly from your D++ cluster instance. * - * @param s Your Discord bot's statistics. - * @return co_await to retrieve a bool - * @note For its C++17 callback-based counterpart, see post_stats. - * @see topgg::stats - * @see topgg::client::start_autoposter + * @param interval The interval between posting in seconds. Defaults to 15 minutes. + * @note This function has no effect if the autoposter is already running. * @see topgg::client::post_stats + * @see topgg::client::stop_autoposter * @since 2.0.0 */ - dpp::async co_post_stats(const stats& s); -#endif + void start_autoposter(time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); /** - * @brief Starts autoposting statistics using data directly from your D++ cluster instance. + * @brief Starts autoposting your Discord bot's statistics using a custom data source. * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * bot.start_autoposter(); - * ``` - * - * @param delay The minimum delay between post requests in seconds. Defaults to 30 minutes. - * @throw std::invalid_argument Throws if the delay argument is shorter than 15 minutes. + * @param source A pointer to a autoposter statistics source located in the heap memory. This pointer must be allocated with new, and it will be deleted once the autoposter thread gets stopped. + * @param callback The callback function to call after every request to the API, successful or not. + * @param interval The interval between posting in seconds. Defaults to 15 minutes. * @note This function has no effect if the autoposter is already running. * @see topgg::client::post_stats * @see topgg::client::stop_autoposter + * @see topgg::post_stats_completion_t + * @see topgg::autoposter_source * @since 2.0.0 */ - void start_autoposter(const time_t delay = 1800); - + void start_autoposter(autoposter_source* source, const post_stats_completion_t& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); + /** - * @brief Starts autoposting statistics. + * @brief Starts autoposting your Discord bot's statistics using a custom data source. * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * bot.start_autoposter([](dpp::cluster& bot_inner) { - * return topgg::stats{...}; - * }); - * ``` - * - * @param callback The callback function that returns the current stats. - * @param delay The minimum delay between post requests in seconds. Defaults to 30 minutes. - * @throw std::invalid_argument Throws if the delay argument is shorter than 15 minutes. + * @param source A pointer to a autoposter statistics source located in the heap memory. This pointer must be allocated with new, and it will be deleted once the autoposter thread gets stopped. + * @param interval The interval between posting in seconds. Defaults to 15 minutes. * @note This function has no effect if the autoposter is already running. - * @see topgg::stats * @see topgg::client::post_stats * @see topgg::client::stop_autoposter + * @see topgg::autoposter_source * @since 2.0.0 */ - void start_autoposter(const custom_autopost_callback_t& callback, const time_t delay = 1800); - + void start_autoposter(autoposter_source* source, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); + /** - * @brief Prematurely stops the autoposter. Calling this function is usually unnecessary as this function is called later in the destructor. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * bot.start_autoposter(); - * - * // ... - * - * bot.stop_autoposter(); - * ``` + * @brief Prematurely stops the autoposter. Calling this function is usually unnecessary as this function will be called in the destructor. * * @note This function has no effect if the autoposter is already stopped. * @see topgg::client::post_stats * @since 2.0.0 */ void stop_autoposter() noexcept; - + /** * @brief The destructor. Stops the autoposter if it's running. */ ~client(); + + friend class bot_query; }; }; // namespace topgg diff --git a/include/topgg/export.h b/include/topgg/export.h new file mode 100644 index 0000000..b4800f4 --- /dev/null +++ b/include/topgg/export.h @@ -0,0 +1,38 @@ +/** + * @module topgg + * @file export.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 + +#if defined(_WIN32) && defined(__TOPGG_API__) +#if defined(DPP_STATIC) && !defined(TOPGG_STATIC) +#define TOPGG_STATIC +#elif defined(TOPGG_STATIC) && !defined(DPP_STATIC) +#define DPP_STATIC +#endif +#endif + +#if defined(_WIN32) && !defined(TOPGG_STATIC) +#ifdef __TOPGG_BUILDING_DLL__ +#ifdef __TOPGG_API__ +#include +#endif +#define TOPGG_EXPORT __declspec(dllexport) +#else +#define TOPGG_EXPORT __declspec(dllimport) +#endif +#else +#define TOPGG_EXPORT +#endif + +#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..2d76ee7 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -1,18 +1,18 @@ /** * @module topgg * @file models.h - * @brief The official C++ wrapper for the Top.gg API. + * @brief A community-maintained C++ API Client for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-07-12 - * @version 2.0.0 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-10-02 + * @version 2.1.0 */ #pragma once #include -#include +#include #include #include #include @@ -27,9 +27,29 @@ #undef _XOPEN_SOURCE #endif +#ifdef __TOPGG_BUILDING__ +#define _TOPGG_SNOWFLAKE_FROM_JSON(j, name) \ + dpp::snowflake{j[#name].template get()} +#endif + +#define TOPGG_BOT_QUERY_SORT(lib_name, api_name) \ + inline bot_query& sort_by_##lib_name() noexcept { \ + m_sort = #api_name; \ + return *this; \ + } + +#define TOPGG_BOT_QUERY_QUERY(type, lib_name, api_name, ...) \ + inline bot_query& lib_name(const type lib_name) { \ + add_query(#api_name, lib_name, __VA_ARGS__); \ + return *this; \ + } + namespace topgg { + class bot_query; + class client; + /** - * @brief Base class of the account data stored in the Top.gg API. + * @brief A Top.gg account. * * @see topgg::bot * @see topgg::user @@ -38,45 +58,46 @@ namespace topgg { */ class TOPGG_EXPORT account { protected: - account(const dpp::json& j); + account(const dpp::json& j, const char* id_key = "id"); public: account() = delete; /** - * @brief The account's Discord ID. + * @brief This account's ID. * * @since 2.0.0 */ dpp::snowflake id; /** - * @brief The account's entire Discord avatar URL. + * @brief This account's avatar URL. * - * @note This avatar URL can be animated if possible. * @since 2.0.0 */ std::string avatar; /** - * @brief The account's username. + * @brief This account's username. * * @since 2.0.0 */ std::string username; /** - * @brief The unix timestamp of when this account was created. + * @brief This account's creation date. * * @since 2.0.0 */ time_t created_at; + + friend class client; }; class client; /** - * @brief Represents voters of a Discord bot. + * @brief A Top.gg voter. * * @see topgg::client::get_voters * @see topgg::client::start_autoposter @@ -94,7 +115,7 @@ namespace topgg { }; /** - * @brief Represents a Discord bot listed on Top.gg. + * @brief A Discord bot listed on Top.gg. * * @see topgg::client::get_bot * @see topgg::account @@ -106,6 +127,13 @@ namespace topgg { public: bot() = delete; + /** + * @brief This bot's Top.gg ID. + * + * @since 2.1.0 + */ + dpp::snowflake topgg_id; + /** * @brief The Discord bot's discriminator. * @@ -114,21 +142,21 @@ namespace topgg { std::string discriminator; /** - * @brief The Discord bot's command prefix. + * @brief This bot's prefix. * * @since 2.0.0 */ std::string prefix; /** - * @brief The Discord bot's short description. + * @brief This bot's short description. * * @since 2.0.0 */ std::string short_description; /** - * @brief The Discord bot's long description, if available. + * @brief This bot's long description. * * @note This long description can contain Markdown and/or HTML. * @since 2.0.0 @@ -136,28 +164,28 @@ namespace topgg { std::optional long_description; /** - * @brief A list of the Discord bot's tags. + * @brief This bot's tags. * * @since 2.0.0 */ std::vector tags; /** - * @brief A link to the Discord bot's website, if available. + * @brief This bot's website URL. * * @since 2.0.0 */ std::optional website; /** - * @brief A link to the Discord bot's GitHub repository, if available. + * @brief This bot's GitHub repository URL. * * @since 2.0.0 */ std::optional github; /** - * @brief A list of the Discord bot's owners, represented in Discord user IDs. + * @brief This bot's owner IDs. * * @since 2.0.0 */ @@ -178,11 +206,11 @@ namespace topgg { std::optional banner; /** - * @brief The unix timestamp of when this Discord bot was approved on Top.gg by a Bot Reviewer. + * @brief This bot's submission date. * - * @since 2.0.0 + * @since 2.1.0 */ - time_t approved_at; + time_t submitted_at; /** * @brief Whether this Discord bot is Top.gg certified or not. @@ -199,21 +227,21 @@ namespace topgg { std::vector shards; /** - * @brief The amount of upvotes this Discord bot has. + * @brief The amount of votes this bot has. * * @since 2.0.0 */ size_t votes; /** - * @brief The amount of upvotes this Discord bot has this month. + * @brief The amount of votes this bot has this month. * * @since 2.0.0 */ size_t monthly_votes; /** - * @brief The Discord bot's support server invite URL, if available. + * @brief This bot's support URL. * * @since 2.0.0 */ @@ -227,11 +255,11 @@ namespace topgg { size_t shard_count; /** - * @brief The invite URL of this Discord bot. + * @brief This bot's invite URL. * * @since 2.0.0 */ - std::string invite; + std::optional invite; /** * @brief The URL of this Discord bot’s Top.gg page. @@ -240,11 +268,40 @@ namespace topgg { */ std::string url; + /** + * @brief This bot's Top.gg vanity code. + * + * @since 2.1.0 + */ + std::optional vanity; + + /** + * @brief This bot's posted server count. + * + * @since 2.1.0 + */ + std::optional server_count; + + /** + * @brief This bot's average review score out of 5. + * + * @since 2.1.0 + */ + double review_score; + + /** + * @brief This bot's review count. + * + * @since 2.1.0 + */ + size_t review_count; + + friend class bot_query; friend class client; }; /** - * @brief Represents a Discord bot’s statistics. + * @brief A Discord bot’s statistics. * * @see topgg::voter * @see topgg::client::get_stats @@ -330,7 +387,7 @@ namespace topgg { class user; /** - * @brief Represents a user's social links, if available. + * @brief A user's social links, if available. * * @see topgg::user * @since 2.0.0 @@ -380,7 +437,7 @@ namespace topgg { }; /** - * @brief Represents a user logged into Top.gg. + * @brief A user logged into Top.gg. * * @see topgg::user_socials * @see topgg::client::get_user @@ -452,4 +509,125 @@ namespace topgg { friend class client; }; -}; // namespace topgg \ No newline at end of file + + /** + * @brief The callback function to call when get_bots completes. + * + * @see topgg::client::get_bots + * @see topgg::bot_query + * @since 2.0.1 + */ + using get_bots_completion_t = std::function>&)>; + + /** + * @brief Configure a Discord bot query before sending it to the API. + * + * @see topgg::client::get_bots + * @since 2.0.1 + */ + class TOPGG_EXPORT bot_query { + client* m_client; + std::unordered_map m_query; + const char* m_sort; + + inline bot_query(client* c) + : m_client(c), m_sort(nullptr) {} + + void add_query(const char* key, const uint16_t value, const uint16_t max); + + public: + bot_query() = delete; + + /** + * @brief Sorts results based on each bot's ID. + * + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ + TOPGG_BOT_QUERY_SORT(id, id); + + /** + * @brief Sorts results based on each bot's submission date. + * + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.1.0 + */ + TOPGG_BOT_QUERY_SORT(submission_date, date); + + /** + * @brief Sorts results based on each bot's monthly vote count. + * + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ + TOPGG_BOT_QUERY_SORT(monthly_votes, monthlyPoints); + + /** + * @brief Sets the maximum amount of bots to be queried. + * + * @param limit The maximum amount of bots to be queried. This cannot be more than 500. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ + TOPGG_BOT_QUERY_QUERY(uint16_t, limit, limit, 500); + + /** + * @brief Sets the amount of bots to be skipped. + * + * @param skip The amount of bots to be skipped. This cannot be more than 499. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ + TOPGG_BOT_QUERY_QUERY(uint16_t, skip, offset, 499); + + /** + * @brief Sends the query to the API. + * + * @param callback The callback function to call when send() completes. + * @note For its C++20 coroutine counterpart, see co_send(). + * @see topgg::client::get_bots + * @see topgg::bot_query::co_send + * @since 2.0.1 + */ + void send(const get_bots_completion_t& callback); + +#ifdef DPP_CORO + /** + * @brief Sends the query to the API through a C++20 coroutine. + * + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. + * @return co_await to retrieve a vector of topgg::bot if successful + * @note For its C++17 callback-based counterpart, see get_bot. + * @see topgg::client::get_bots + * @see topgg::bot_query::send + * @since 2.0.1 + */ + topgg::async_result> co_send(); +#endif + + friend class client; + }; + + /** + * @brief An abstract interface for bots that have a custom way of retrieving their statistics. + * + * @see topgg::start_autoposter + * @since 2.1.0 + */ + class autoposter_source { + public: + virtual stats TOPGG_EXPORT get_stats(dpp::cluster&) = 0; + }; +}; // namespace topgg + +#undef TOPGG_BOT_QUERY_SEARCH +#undef TOPGG_BOT_QUERY_QUERY +#undef TOPGG_BOT_QUERY_SORT \ No newline at end of file diff --git a/include/topgg/result.h b/include/topgg/result.h index f0e3764..9de8ee4 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -1,11 +1,11 @@ /** * @module topgg * @file result.h - * @brief The official C++ wrapper for the Top.gg API. + * @brief A community-maintained C++ API Client for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-07-12 - * @version 2.0.0 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-10-02 + * @version 2.1.0 */ #pragma once @@ -21,7 +21,7 @@ namespace topgg { class internal_result; /** - * @brief An exception that gets thrown when the client receives an unexpected error from Top.gg's end. + * @brief Unexpected error from Top.gg's end. * * @since 2.0.0 */ @@ -33,19 +33,20 @@ namespace topgg { }; /** - * @brief An exception that gets thrown when its known that the client uses an invalid Top.gg API token. + * @brief Invalid API token. * * @since 2.0.0 */ class invalid_token: public std::invalid_argument { inline invalid_token() - : std::invalid_argument("Invalid Top.gg API token.") {} + : std::invalid_argument("Invalid API token.") {} + friend class client; friend class internal_result; }; /** - * @brief An exception that gets thrown when such query does not exist. + * @brief Such query does not exist. * * @since 2.0.0 */ @@ -57,7 +58,7 @@ namespace topgg { }; /** - * @brief An exception that gets thrown when the client gets ratelimited from sending more HTTP requests. + * @brief Ratelimited from sending more requests. * * @since 2.0.0 */ @@ -67,70 +68,73 @@ namespace topgg { public: /** - * @brief The amount of seconds before the ratelimit is lifted. + * @brief How long the client should wait (in seconds) before it can make a request to the API again. * * @since 2.0.0 */ const uint16_t retry_after; - + ratelimited() = delete; friend class internal_result; }; - + template class result; - class TOPGG_EXPORT internal_result { + class internal_result { const dpp::http_request_completion_t m_response; - void prepare() const; + static void handle_response(const dpp::http_request_completion_t& response); - inline internal_result(const dpp::http_request_completion_t& response) - : m_response(response) {} - - public: - internal_result() = delete; + inline internal_result(): m_response() {} + inline internal_result(const dpp::http_request_completion_t& response): m_response(response) {} template friend class result; + friend class client; }; - + class client; /** - * @brief A result class that gets returned from every HTTP response. - * This class may either contain the desired data or an error. + * @brief The desired data or an error. * * @see topgg::async_result * @since 2.0.0 */ template - class TOPGG_EXPORT result { + class result { const internal_result m_internal; - const std::function m_parse_fn; + const std::variant, T> m_data; - inline result(const dpp::http_request_completion_t& response, const std::function& parse_fn) - : m_internal(response), m_parse_fn(parse_fn) {} + inline result(const T& data): m_data(data) {} + inline result(const dpp::http_request_completion_t& response, const std::function& parse_fn): m_internal(response), m_data(parse_fn) {} public: result() = delete; /** - * @brief Tries to retrieve the returned data inside. + * @brief Tries to retrieve the 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. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::not_found Such query does not exist. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. + * @return T The desired data. * @since 2.0.0 */ T get() const { - m_internal.prepare(); + try { + const auto parse_fn{std::get>(m_data)}; - return m_parse_fn(dpp::json::parse(m_internal.m_response.body)); + internal_result::handle_response(m_internal.m_response); + + return parse_fn(dpp::json::parse(m_internal.m_response.body)); + } catch (TOPGG_UNUSED const std::bad_variant_access&) { + return std::get(m_data); + } } friend class client; @@ -138,8 +142,7 @@ namespace topgg { #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. + * @brief The desired data from a C++20 coroutine or an error. * * @see topgg::result * @since 2.0.0 @@ -147,13 +150,14 @@ namespace topgg { template class TOPGG_EXPORT async_result { dpp::async> m_fut; - + template - inline async_result(F&& cb): m_fut(std::forward(cb)) {} - + inline async_result(F&& cb) + : m_fut(std::forward(cb)) {} + public: async_result() = delete; - + /** * @brief This object can't be copied. * @@ -178,7 +182,7 @@ namespace topgg { * @since 2.0.0 */ async_result& operator=(const async_result& other) = delete; - + /** * @brief Moves data from another object. * @@ -187,55 +191,55 @@ namespace topgg { * @since 2.0.0 */ async_result& operator=(async_result&& other) noexcept = default; - + /** - * @brief Suspends the caller and tries to retrieve the fetched data. + * @brief Suspends the caller and tries to retrieve the 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. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::not_found Such query does not exist. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. + * @return T The desired data. * @see topgg::result::get * @since 2.0.0 */ inline T& operator co_await() & { return m_fut.operator co_await().get(); } - + /** - * @brief Suspends the caller and tries to retrieve the fetched data. + * @brief Suspends the caller and tries to retrieve the 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. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::not_found Such query does not exist. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. + * @return T The desired data. * @see topgg::result::get * @since 2.0.0 */ - inline const T& operator co_await() const & { + 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. + * @throw topgg::internal_server_error Unexpected error from Top.gg's end. + * @throw topgg::invalid_token Invalid API token. + * @throw topgg::not_found Such query does not exist. + * @throw topgg::ratelimited Ratelimited from sending more requests. + * @throw dpp::http_error An unexpected HTTP exception has occured. + * @return T The desired data. * @see topgg::result::get * @since 2.0.0 */ inline T&& operator co_await() && { return std::forward>>(m_fut).operator co_await().get(); } - + friend class client; }; #endif diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index eb92c90..54ab057 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -1,38 +1,23 @@ /** * @module topgg * @file topgg.h - * @brief The official C++ wrapper for the Top.gg API. + * @brief A community-maintained C++ API Client for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-09-22 - * @version 2.0.0 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-10-02 + * @version 2.1.0 */ #pragma once -#ifdef _WIN32 -#if defined(DPP_STATIC) && !defined(TOPGG_STATIC) -#define TOPGG_STATIC -#elif defined(TOPGG_STATIC) && !defined(DPP_STATIC) -#define DPP_STATIC -#endif -#endif - -#if defined(_WIN32) && !defined(TOPGG_STATIC) -#ifdef __TOPGG_BUILDING_DLL__ -#include -#define TOPGG_EXPORT __declspec(dllexport) -#else -#define TOPGG_EXPORT __declspec(dllimport) -#endif -#else -#define TOPGG_EXPORT -#endif +#define __TOPGG_API__ +#include +#undef __TOPGG_API__ -#if defined(__GNUC__) || defined(__clang__) -#define TOPGG_UNUSED __attribute__((unused)) +#ifdef __TOPGG_TESTING__ +#define TOPGG_AUTOPOSTER_MIN_INTERVAL 2 #else -#define TOPGG_UNUSED +#define TOPGG_AUTOPOSTER_MIN_INTERVAL 900 #endif #ifdef __clang__ @@ -48,6 +33,8 @@ #pragma clang diagnostic pop #endif +#define TOPGG_BASE_URL "https://top.gg/api" + #include #include #include \ No newline at end of file diff --git a/src/client.cpp b/src/client.cpp index a94ee2d..5b57899 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -2,27 +2,122 @@ using topgg::client; -client::client(dpp::cluster& cluster, const std::string& token): m_token(token), m_cluster(cluster), m_autoposter_timer(0) { +// clang-format off +static constexpr unsigned char g_base64_decoding_table[] = { + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 62, 64, 64, 64, 63, 52, 53, 54, 55, 56, 57, + 58, 59, 60, 61, 64, 64, 64, 64, 64, 64, 64, 0, 1, 2, 3, 4, 5, 6, + 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 64, 64, 64, 64, 64, 64, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, + 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64, + 64, 64, 64, 64 +}; +// clang-format on + +static bool base64_decode(const std::string& input, std::string& output) { + const auto input_size{input.size()}; + + if (input_size % 4 != 0) { + return false; + } + + auto output_size{input_size / 4 * 3}; + + if (input_size >= 1 && input[input_size - 1] == '=') { + output_size--; + } + + if (input_size >= 2 && input[input_size - 2] == '=') { + output_size--; + } + + output.resize(output_size); + + uint32_t a, b, c, d, triple{}; + + for (size_t i = 0, j = 0; i < input_size;) { + a = input[i] == '=' ? 0 & i++ : g_base64_decoding_table[static_cast(input[i++])]; + b = input[i] == '=' ? 0 & i++ : g_base64_decoding_table[static_cast(input[i++])]; + c = input[i] == '=' ? 0 & i++ : g_base64_decoding_table[static_cast(input[i++])]; + d = input[i] == '=' ? 0 & i++ : g_base64_decoding_table[static_cast(input[i++])]; + + triple = (a << 3 * 6) + (b << 2 * 6) + (c << 1 * 6) + (d << 0 * 6); + + if (j < output_size) { + output[j++] = (triple >> 2 * 8) & 0xFF; + } + + if (j < output_size) { + output[j++] = (triple >> 1 * 8) & 0xFF; + } + + if (j < output_size) { + output[j++] = (triple >> 0 * 8) & 0xFF; + } + } + + return true; +} + +static dpp::json parse_api_token(const std::string& token) { + const auto first_pos{token.find('.')}; + + if (first_pos != std::string::npos) { + const auto first_token_slice{token.substr(first_pos + 1)}; + const auto second_pos{first_token_slice.find('.')}; + + if (second_pos != std::string::npos) { + auto base64_input{first_token_slice.substr(0, second_pos)}; + const auto additional_equals{4 - (base64_input.length() % 4)}; + + for (size_t i{}; i < additional_equals; i++) { + base64_input.push_back('='); + } + + std::string base64_decoded{}; + + if (base64_decode(base64_input, base64_decoded)) { + return dpp::json::parse(base64_decoded); + } + } + } + + throw std::invalid_argument{"Got a malformed API token."}; +} + +client::client(dpp::cluster& cluster, const std::string& token) + : m_cluster(cluster), m_autoposter_timer(0) { + const auto token_data{parse_api_token(token)}; + + m_id = _TOPGG_SNOWFLAKE_FROM_JSON(token_data, id); + m_headers.insert(std::pair("Authorization", "Bearer " + token)); - m_headers.insert(std::pair("Connection", "close")); m_headers.insert(std::pair("Content-Type", "application/json")); m_headers.insert(std::pair("User-Agent", "topgg (https://github.com/top-gg-community/cpp-sdk) D++")); } void client::get_bot(const dpp::snowflake bot_id, const topgg::get_bot_completion_t& callback) { - basic_request("/bots/" + std::to_string(bot_id), callback, [](const auto& j) { + basic_request(dpp::m_get, "/bots/" + bot_id.str(), callback, [](const auto& j) { return topgg::bot{j}; }); } #ifdef DPP_CORO topgg::async_result client::co_get_bot(const dpp::snowflake bot_id) { - return topgg::async_result{ [this, bot_id] (C&& cc) { return get_bot(bot_id, std::forward(cc)); }}; + return topgg::async_result{[this, bot_id](C&& cc) { return get_bot(bot_id, std::forward(cc)); }}; } #endif void client::get_user(const dpp::snowflake user_id, const topgg::get_user_completion_t& callback) { - basic_request("/users/" + std::to_string(user_id), callback, [](const auto& j) { + basic_request(dpp::m_get, "/users/" + user_id.str(), callback, [](const auto& j) { return topgg::user{j}; }); } @@ -33,46 +128,59 @@ topgg::async_result client::co_get_user(const dpp::snowflake user_i } #endif -void client::post_stats(const topgg::post_stats_completion_t& callback) { - post_stats(stats{m_cluster}, callback); +topgg::stats client::get_stats() { +#ifdef __TOPGG_TESTING__ + return topgg::stats{2}; +#else + return topgg::stats{m_cluster}; +#endif } -#ifdef DPP_CORO -dpp::async client::co_post_stats() { - return dpp::async{ [this] (C&& cc) { return post_stats(stats{m_cluster}, std::forward(cc)); }}; +void client::post_stats_inner(const size_t server_count, const dpp::http_completion_event callback) { + dpp::json j{}; + j["server_count"] = server_count; + + request(dpp::m_post, "/bots/stats", callback, j.dump()); } -#endif -void client::post_stats(const stats& s, const topgg::post_stats_completion_t& callback) { - auto headers = std::multimap{m_headers}; - const auto s_json = s.to_json(); +void client::post_stats(const topgg::post_stats_completion_t& callback) { + const auto stats{get_stats()}; + const auto server_count{stats.server_count().value_or(0)}; - headers.insert(std::pair("Content-Length", std::to_string(s_json.size()))); + if (server_count <= 0) { + return callback(false); + } - m_cluster.request("https://top.gg/api/bots/stats", dpp::m_post, [callback](const auto& response) { callback(response.error == dpp::h_success && response.status < 400); }, s_json, "application/json", headers); + post_stats_inner(server_count, [callback](const auto& response) { + callback(response.error == dpp::h_success && response.status < 400); + }); } #ifdef DPP_CORO -dpp::async client::co_post_stats(const stats& s) { - return dpp::async{ [this, s] (C&& cc) { return post_stats(s, std::forward(cc)); }}; +dpp::async client::co_post_stats() { + return dpp::async{[this](C&& cc) { return post_stats(std::forward(cc)); }}; } #endif void client::get_stats(const topgg::get_stats_completion_t& callback) { - basic_request("/bots/stats", callback, [](const auto& j) { + basic_request(dpp::m_get, "/bots/stats", callback, [](const auto& j) { return topgg::stats{j}; }); } #ifdef DPP_CORO topgg::async_result client::co_get_stats() { - return topgg::async_result{ [this] (C&& cc) { return get_stats(std::forward(cc)); }}; + return topgg::async_result{[this](C&& cc) { return get_stats(std::forward(cc)); }}; } #endif -void client::get_voters(const topgg::get_voters_completion_t& callback) { - basic_request>("/bots/votes", callback, [](const auto& j) { - std::vector voters; +void client::get_voters(size_t page, const topgg::get_voters_completion_t& callback) { + if (page < 1) { + page = 1; + } + + basic_request>(dpp::m_get, "/bots/" + m_id.str() + "/votes?page=" + std::to_string(page), callback, [](const auto& j) { + std::vector voters{}; for (const auto& part: j) { voters.push_back(topgg::voter{part}); @@ -82,67 +190,85 @@ void client::get_voters(const topgg::get_voters_completion_t& callback) { }); } +void client::get_voters(const topgg::get_voters_completion_t& callback) { + get_voters(1, callback); +} + #ifdef DPP_CORO -topgg::async_result> client::co_get_voters() { - return topgg::async_result>{ [this] (C&& cc) { return get_voters(std::forward(cc)); }}; +topgg::async_result> client::co_get_voters(size_t page) { + return topgg::async_result>{[this, page](C&& cc) { return get_voters(page, std::forward(cc)); }}; } #endif - void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_completion_t& callback) { - basic_request("/bots/votes?userId=" + std::to_string(user_id), callback, [](const auto& j) { + basic_request(dpp::m_get, "/bots/check?userId=" + user_id.str(), callback, [](const auto& j) { return j["voted"].template get() != 0; }); } #ifdef DPP_CORO topgg::async_result client::co_has_voted(const dpp::snowflake user_id) { - return topgg::async_result{ [user_id, this] (C&& cc) { return has_voted(user_id, std::forward(cc)); }}; + return topgg::async_result{[user_id, this](C&& cc) { return has_voted(user_id, std::forward(cc)); }}; } #endif void client::is_weekend(const topgg::is_weekend_completion_t& callback) { - basic_request("/weekend", callback, [](const auto& j) { + basic_request(dpp::m_get, "/weekend", callback, [](const auto& j) { return j["is_weekend"].template get(); }); } #ifdef DPP_CORO topgg::async_result client::co_is_weekend() { - return topgg::async_result{ [this] (C&& cc) { return is_weekend(std::forward(cc)); }}; + return topgg::async_result{[this](C&& cc) { return is_weekend(std::forward(cc)); }}; } #endif -void client::start_autoposter(const time_t delay) { - start_autoposter([](dpp::cluster& bot) { - return stats{bot}; - }, delay); +void client::start_autoposter(const topgg::post_stats_completion_t& callback, time_t interval) { + start_autoposter([this](TOPGG_UNUSED const auto&) { + return get_stats(); + }, callback, interval); } -void client::start_autoposter(const topgg::custom_autopost_callback_t& callback, const time_t delay) { - /** - * Check the timer duration is not less than 15 minutes - */ - if (delay < 15 * 60) { - throw std::invalid_argument{"Delay mustn't be shorter than 15 minutes."}; +void client::start_autoposter(const topgg::custom_autopost_callback_t& stats_callback, const topgg::post_stats_completion_t& post_callback, time_t interval) { + if (interval < TOPGG_AUTOPOSTER_MIN_INTERVAL) { + interval = TOPGG_AUTOPOSTER_MIN_INTERVAL; } - + /** * Create a D++ timer, this is managed by the D++ cluster and ticks every n seconds. * It can be stopped at any time without blocking, and does not need to create extra threads. */ if (!m_autoposter_timer) { - m_autoposter_timer = m_cluster.start_timer([this, callback](TOPGG_UNUSED dpp::timer) { - const auto s = callback(m_cluster); - const auto s_json = s.to_json(); - std::multimap headers{m_headers}; - headers.insert(std::pair("Content-Length", std::to_string(s_json.length()))); - - m_cluster.request("https://top.gg/api/bots/stats", dpp::m_post, [](TOPGG_UNUSED const auto&) {}, s_json, "application/json", headers); - }, delay); + // clang-format off + m_autoposter_timer = m_cluster.start_timer([this, stats_callback, post_callback](TOPGG_UNUSED dpp::timer) { + const auto stats{stats_callback(m_cluster)}; + const auto server_count{stats.server_count().value_or(0)}; + + if (server_count > 0) { + post_stats_inner(server_count, [post_callback](TOPGG_UNUSED const auto& response) { + post_callback(response.error == dpp::h_success && response.status < 400); + }); + } + }, interval); + // clang-format on } } +void client::start_autoposter(const time_t interval) { + start_autoposter([](TOPGG_UNUSED const auto) {}, interval); +} + +void client::start_autoposter(topgg::autoposter_source* source, const topgg::post_stats_completion_t& callback, time_t interval) { + start_autoposter([source](auto& cluster) { + return source->get_stats(cluster); + }, callback, interval); +} + +void client::start_autoposter(topgg::autoposter_source* source, time_t interval) { + start_autoposter(source, [](TOPGG_UNUSED const auto) {}, interval); +} + void client::stop_autoposter() noexcept { if (m_autoposter_timer) { m_cluster.stop_timer(m_autoposter_timer); diff --git a/src/models.cpp b/src/models.cpp index 7975681..0073401 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -1,93 +1,133 @@ #include -using topgg::account; -using topgg::bot; -using topgg::stats; -using topgg::user; -using topgg::user_socials; +#include #ifdef _WIN32 #include #include -static void strptime(const char* s, const char* f, tm* t) { - std::istringstream input{s}; - input.imbue(std::locale{setlocale(LC_ALL, nullptr)}); - input >> std::get_time(t, f); -} - #ifdef _MSC_VER #pragma warning(disable: 4101) #endif #endif -#define SERIALIZE_PRIVATE_OPTIONAL(j, name) \ - if (m_##name.has_value()) { \ - j[#name] = m_##name.value(); \ +#define _TOPGG_SERIALIZE_PRIVATE_OPTIONAL(j, name) \ + if (m_##name.has_value()) { \ + j[#name] = m_##name.value(); \ } -#define DESERIALIZE(j, name, type) \ +#define _TOPGG_DESERIALIZE(j, name, type) \ name = j[#name].template get() -#define DESERIALIZE_ALIAS(j, name, prop, type) \ +#define _TOPGG_DESERIALIZE_ALIAS(j, name, prop, type) \ prop = j[#name].template get() -#define IGNORE_EXCEPTION(scope) \ +#define _TOPGG_IGNORE_EXCEPTION(scope) \ try scope catch (TOPGG_UNUSED const std::exception&) {} -#define DESERIALIZE_VECTOR(j, name, type) \ - IGNORE_EXCEPTION({ \ - name = j[#name].template get>(); \ +#define _TOPGG_DESERIALIZE_VECTOR(j, name, type) \ + _TOPGG_IGNORE_EXCEPTION({ \ + name = j[#name].template get>(); \ }) -#define DESERIALIZE_VECTOR_ALIAS(j, name, prop, type) \ - IGNORE_EXCEPTION({ \ - prop = j[#name].template get>(); \ +#define _TOPGG_DESERIALIZE_VECTOR_ALIAS(j, name, prop, type) \ + _TOPGG_IGNORE_EXCEPTION({ \ + prop = j[#name].template get>(); \ }) -#define DESERIALIZE_OPTIONAL(j, name, type) \ - IGNORE_EXCEPTION({ \ - name = j[#name].template get(); \ +#define _TOPGG_DESERIALIZE_OPTIONAL(j, name, type) \ + _TOPGG_IGNORE_EXCEPTION({ \ + name = j[#name].template get(); \ }) -#define DESERIALIZE_PRIVATE_OPTIONAL(j, name, type) \ - IGNORE_EXCEPTION({ \ - m_##name = j[#name].template get(); \ +#define _TOPGG_DESERIALIZE_PRIVATE_OPTIONAL(j, name, type) \ + _TOPGG_IGNORE_EXCEPTION({ \ + m_##name = j[#name].template get(); \ }) -#define DESERIALIZE_OPTIONAL_ALIAS(j, name, prop) \ - IGNORE_EXCEPTION({ \ - prop = j[#name].template get(); \ +#define _TOPGG_DESERIALIZE_OPTIONAL_ALIAS(j, name, prop) \ + _TOPGG_IGNORE_EXCEPTION({ \ + prop = j[#name].template get(); \ }) -#define DESERIALIZE_OPTIONAL_STRING(j, name) \ - IGNORE_EXCEPTION({ \ - const auto value = j[#name].template get(); \ - \ - if (value.size() > 0) { \ - name = std::optional{value}; \ - } \ +#define _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, name) \ + _TOPGG_IGNORE_EXCEPTION({ \ + const auto value{j[#name].template get()}; \ + \ + if (value.size() > 0) { \ + name = std::optional{value}; \ + } \ }) -#define DESERIALIZE_OPTIONAL_STRING_ALIAS(j, name, prop) \ - IGNORE_EXCEPTION({ \ - const auto value = j[#name].template get(); \ +#define _TOPGG_DESERIALIZE_OPTIONAL_STRING_ALIAS(j, name, prop) \ + _TOPGG_IGNORE_EXCEPTION({ \ + const auto value{j[#name].template get()}; \ \ if (value.size() > 0) { \ prop = std::optional{value}; \ } \ }) -account::account(const dpp::json& j) { - id = dpp::snowflake{j["id"].template get()}; +using topgg::account; +using topgg::bot; +using topgg::bot_query; +using topgg::stats; +using topgg::user; +using topgg::user_socials; - DESERIALIZE(j, username, std::string); +static void strptime(const char* s, const char* f, tm* t) { + std::istringstream input{s}; + input.imbue(std::locale{setlocale(LC_ALL, nullptr)}); + input >> std::get_time(t, f); +} - try { - const auto hash = j["avatar"].template get(); - const char* ext = hash.rfind("a_", 0) == 0 ? "gif" : "png"; +static time_t parse_vote_time(const dpp::json& j, const char* key) { + auto j_text{j[key].template get()}; + tm text_tm{}; + + const auto dot_pos{j_text.find('.')}; + + if (dot_pos != std::string::npos) { + j_text = j_text.substr(0, dot_pos); + } + + strptime(j_text.data(), "%Y-%m-%dT%H:%M:%S", &text_tm); + + return mktime(&text_tm); +} + +static time_t timestamp_from_id(const dpp::snowflake& id) { + return static_cast(((id >> 22) / 1000) + 1420070400); +} + +static std::string querystring(const std::string& value) { + static constexpr char hex[] = "0123456789abcdef"; + std::string output{}; + + output.reserve(value.length()); - avatar = "https://cdn.discordapp.com/avatars/" + std::to_string(id) + "/" + hash + "." + ext + "?size=1024"; + for (size_t i{}; i < value.length(); i++) { + const auto c{value[i]}; + + if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) { + output.push_back(c); + } else { + output.push_back('%'); + output.push_back(hex[(c >> 4) & 0x0f]); + output.push_back(hex[c & 0x0f]); + } + } + + return output; +} + +account::account(const dpp::json& j, const char* id_key) { + id = dpp::snowflake{j[id_key].template get()}; + + _TOPGG_DESERIALIZE(j, username, std::string); + + try { + _TOPGG_DESERIALIZE(j, avatar, std::string); } catch (TOPGG_UNUSED const std::exception&) { avatar = "https://cdn.discordapp.com/embed/avatars/" + std::to_string((id >> 22) % 5) + ".png"; } @@ -95,18 +135,23 @@ account::account(const dpp::json& j) { created_at = static_cast(((id >> 22) / 1000) + 1420070400); } -bot::bot(const dpp::json& j) - : account(j), url("https://top.gg/bot/") { - DESERIALIZE(j, discriminator, std::string); - DESERIALIZE(j, prefix, std::string); - DESERIALIZE_ALIAS(j, shortdesc, short_description, std::string); - DESERIALIZE_OPTIONAL_STRING_ALIAS(j, longdesc, long_description); - DESERIALIZE_VECTOR(j, tags, std::string); - DESERIALIZE_OPTIONAL_STRING(j, website); - DESERIALIZE_OPTIONAL_STRING(j, github); +bot::bot(const dpp::json& j) : account(j, "clientid") { + topgg_id = _TOPGG_SNOWFLAKE_FROM_JSON(j, id); - IGNORE_EXCEPTION({ - const auto j_owners = j["owners"].template get>(); + _TOPGG_DESERIALIZE(j, username, std::string); + _TOPGG_DESERIALIZE(j, avatar, std::string); + + created_at = timestamp_from_id(id); + + _TOPGG_DESERIALIZE(j, prefix, std::string); + _TOPGG_DESERIALIZE_ALIAS(j, shortdesc, short_description, std::string); + _TOPGG_DESERIALIZE_OPTIONAL_STRING_ALIAS(j, longdesc, long_description); + _TOPGG_DESERIALIZE_VECTOR(j, tags, std::string); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, website); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, github); + + _TOPGG_IGNORE_EXCEPTION({ + const auto j_owners{j["owners"].template get>()}; owners.reserve(j_owners.size()); @@ -115,52 +160,71 @@ bot::bot(const dpp::json& j) } }); - DESERIALIZE_VECTOR(j, guilds, size_t); - DESERIALIZE_OPTIONAL_STRING_ALIAS(j, bannerUrl, banner); + const auto j_submitted_at{j["date"].template get()}; + tm submitted_at_tm{}; - const auto j_approved_at = j["date"].template get(); - tm approved_at_tm; + strptime(j_submitted_at.data(), "%Y-%m-%dT%H:%M:%S", &submitted_at_tm); + submitted_at = mktime(&submitted_at_tm); - strptime(j_approved_at.data(), "%Y-%m-%dT%H:%M:%S", &approved_at_tm); - approved_at = mktime(&approved_at_tm); + _TOPGG_DESERIALIZE_ALIAS(j, points, votes, size_t); + _TOPGG_DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); + _TOPGG_DESERIALIZE_OPTIONAL(j, invite, std::string); + _TOPGG_DESERIALIZE_OPTIONAL(j, vanity, std::string); + _TOPGG_DESERIALIZE_OPTIONAL(j, support, std::string); + _TOPGG_DESERIALIZE_OPTIONAL(j, server_count, size_t); + + const auto reviews{j["reviews"]}; - DESERIALIZE_ALIAS(j, certifiedBot, is_certified, bool); - DESERIALIZE_VECTOR(j, shards, size_t); - DESERIALIZE_ALIAS(j, points, votes, size_t); - DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); + _TOPGG_DESERIALIZE_ALIAS(reviews, averageScore, review_score, double); + _TOPGG_DESERIALIZE_ALIAS(reviews, count, review_count, size_t); +} - try { - DESERIALIZE(j, invite, std::string); - } catch (TOPGG_UNUSED const std::exception&) { - invite = "https://discord.com/oauth2/authorize?scope=bot&client_id=" + std::to_string(id); +void bot_query::add_query(const char* key, const uint16_t value, const uint16_t max) { + m_query.insert_or_assign(key, std::to_string(std::min(value, max))); +} + +void bot_query::send(const topgg::get_bots_completion_t& callback) { + std::string path{"/bots?"}; + + if (m_sort != nullptr) { + path.append("sort="); + path.append(m_sort); + path.push_back('&'); } - IGNORE_EXCEPTION({ - const auto j_support = j["support"].template get(); + for (const auto& additional_query: m_query) { + path.append(additional_query.first); + path.push_back('='); + path.append(additional_query.second); + path.push_back('&'); + } + + path.pop_back(); + + m_client->basic_request>(dpp::m_get, path, callback, [](const auto& j) { + std::vector bots{}; - if (j_support.size() > 0) { - support = std::optional{"https://discord.com/invite/" + j_support}; + bots.reserve(j["count"].template get()); + + for (const auto& bot: j["results"].template get>()) { + bots.push_back(topgg::bot{bot}); } - }); - try { - DESERIALIZE(j, shard_count, size_t); - } catch (TOPGG_UNUSED const std::exception&) { - shard_count = shards.size(); - } + return bots; + }); +} - try { - url.append(j["vanity"].template get()); - } catch (TOPGG_UNUSED const std::exception&) { - url.append(std::to_string(id)); - } +#ifdef DPP_CORO +dpp::async> bot_query::co_send() { + return dpp::async>{[this](C&& cc) { return send(std::forward(cc)); }}; } +#endif 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); + _TOPGG_DESERIALIZE_PRIVATE_OPTIONAL(j, shard_count, size_t); + _TOPGG_DESERIALIZE_PRIVATE_OPTIONAL(j, server_count, size_t); + _TOPGG_DESERIALIZE_PRIVATE_OPTIONAL(j, shards, std::vector); + _TOPGG_DESERIALIZE_PRIVATE_OPTIONAL(j, shard_id, size_t); } stats::stats(dpp::cluster& bot) { @@ -195,10 +259,10 @@ stats::stats(const std::vector& shards, const size_t shard_index) std::string stats::to_json() const { dpp::json j; - SERIALIZE_PRIVATE_OPTIONAL(j, shard_count); - SERIALIZE_PRIVATE_OPTIONAL(j, server_count); - SERIALIZE_PRIVATE_OPTIONAL(j, shards); - SERIALIZE_PRIVATE_OPTIONAL(j, shard_id); + _TOPGG_SERIALIZE_PRIVATE_OPTIONAL(j, shard_count); + _TOPGG_SERIALIZE_PRIVATE_OPTIONAL(j, server_count); + _TOPGG_SERIALIZE_PRIVATE_OPTIONAL(j, shards); + _TOPGG_SERIALIZE_PRIVATE_OPTIONAL(j, shard_id); return j.dump(); } @@ -215,7 +279,7 @@ std::optional stats::server_count() const noexcept { if (m_server_count.has_value()) { return m_server_count; } else { - IGNORE_EXCEPTION({ + _TOPGG_IGNORE_EXCEPTION({ const auto& shards = m_shards.value(); if (shards.size() > 0) { @@ -228,25 +292,24 @@ std::optional stats::server_count() const noexcept { } user_socials::user_socials(const dpp::json& j) { - DESERIALIZE_OPTIONAL_STRING(j, github); - DESERIALIZE_OPTIONAL_STRING(j, instagram); - DESERIALIZE_OPTIONAL_STRING(j, reddit); - DESERIALIZE_OPTIONAL_STRING(j, twitter); - DESERIALIZE_OPTIONAL_STRING(j, youtube); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, github); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, instagram); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, reddit); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, twitter); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, youtube); } -user::user(const dpp::json& j) - : account(j) { - DESERIALIZE_OPTIONAL_STRING(j, bio); - DESERIALIZE_OPTIONAL_STRING(j, banner); +user::user(const dpp::json& j) : account(j) { + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, bio); + _TOPGG_DESERIALIZE_OPTIONAL_STRING(j, banner); if (j.contains("socials")) { socials = std::optional{user_socials{j["socials"].template get()}}; } - DESERIALIZE_ALIAS(j, supporter, is_supporter, bool); - DESERIALIZE_ALIAS(j, certifiedDev, is_certified_dev, bool); - DESERIALIZE_ALIAS(j, mod, is_moderator, bool); - DESERIALIZE_ALIAS(j, webMod, is_web_moderator, bool); - DESERIALIZE_ALIAS(j, admin, is_admin, bool); -} + _TOPGG_DESERIALIZE_ALIAS(j, supporter, is_supporter, bool); + _TOPGG_DESERIALIZE_ALIAS(j, certifiedDev, is_certified_dev, bool); + _TOPGG_DESERIALIZE_ALIAS(j, mod, is_moderator, bool); + _TOPGG_DESERIALIZE_ALIAS(j, webMod, is_web_moderator, bool); + _TOPGG_DESERIALIZE_ALIAS(j, admin, is_admin, bool); +} \ No newline at end of file diff --git a/src/result.cpp b/src/result.cpp index 3719440..d82bffb 100644 --- a/src/result.cpp +++ b/src/result.cpp @@ -60,26 +60,29 @@ using topgg::ratelimited; #pragma clang diagnostic pop #endif -void internal_result::prepare() const { - if (m_response.error != dpp::h_success) { - throw m_response.error; - } else if (m_response.status >= 400) { - 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 retry_after = j["retry_after"].template get(); - - throw ratelimited{retry_after}; - } - - default: - throw internal_server_error{}; +void internal_result::handle_response(const dpp::http_request_completion_t& response) { + if (response.error != dpp::h_success) { + throw response.error; + } else if (response.status >= 400) { + switch (response.status) { + case 401: { + throw invalid_token{}; + } + + case 404: { + throw not_found{}; + } + + case 429: { + const auto j{json::parse(response.body)}; + const auto retry_after{j["retry_after"].template get()}; + + throw ratelimited{retry_after}; + } + + default: { + throw internal_server_error{}; + } } } } \ No newline at end of file diff --git a/topgg.rc b/topgg.rc index fc4d2cd..16071c8 100644 --- a/topgg.rc +++ b/topgg.rc @@ -16,12 +16,12 @@ BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", "Top.gg" - VALUE "FileDescription", "The official C++ wrapper for the Top.gg API." - VALUE "FileVersion", "2.0.0" - VALUE "ProductVersion", "2.0.0" + VALUE "FileDescription", "A community-maintained C++ API Client for the Top.gg API." + VALUE "FileVersion", "2.1.0" + VALUE "ProductVersion", "2.1.0" VALUE "ProductName", "Top.gg C++ SDK" VALUE "InternalName", "Top.gg C++ SDK" - VALUE "LegalCopyright", "Copyright (c) 2024 Top.gg & null8626" + VALUE "LegalCopyright", "Copyright (c) 2024-2025 Top.gg & null8626" VALUE "OriginalFilename", "topgg.dll" END END