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/.gitmodules b/.gitmodules index 10beb96..3827952 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,3 +1,6 @@ [submodule "docs/doxygen-awesome-css"] path = docs/doxygen-awesome-css url = https://github.com/jothepro/doxygen-awesome-css.git +[submodule "deps/drogon"] + path = deps/drogon + url = https://github.com/drogonframework/drogon.git \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d97a2b..1a20fba 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,30 +1,45 @@ -cmake_minimum_required(VERSION 3.8.2) +cmake_minimum_required(VERSION 3.15) project( topgg LANGUAGES CXX HOMEPAGE_URL "https://docs.top.gg/docs" - DESCRIPTION "The official C++ wrapper for the Top.gg API." + DESCRIPTION "A simple API wrapper for Top.gg written in C++." ) -set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type") +set(CMAKE_VERBOSE_MAKEFILE ON) +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type") + option(BUILD_SHARED_LIBS "Build shared libraries" ON) -option(ENABLE_CORO "Support for C++20 coroutines" OFF) +option(ENABLE_API "Build primary API support" ON) +option(ENABLE_CPP_HTTPLIB_WEBHOOKS "Build support for webhooks via cpp-httplib" OFF) +option(ENABLE_DROGON_WEBHOOKS "Build support for webhooks via drogon" OFF) +option(ENABLE_CORO "Add support for C++20 coroutines" OFF) +option(TESTING "Enable this only if you are testing the library" OFF) +if(ENABLE_API) file(GLOB TOPGG_SOURCE_FILES src/*.cpp) +endif() + +if(ENABLE_CPP_HTTPLIB_WEBHOOKS) +set(TOPGG_SOURCE_FILES ${TOPGG_SOURCE_FILES} src/webhooks/cpp-httplib.cpp src/webhooks/models.cpp) +elseif(ENABLE_DROGON_WEBHOOKS) +set(TOPGG_SOURCE_FILES ${TOPGG_SOURCE_FILES} src/webhooks/drogon.cpp src/webhooks/models.cpp) +endif() if(BUILD_SHARED_LIBS) add_library(topgg SHARED ${TOPGG_SOURCE_FILES}) if(WIN32) -target_sources(topgg PRIVATE ${CMAKE_SOURCE_DIR}/topgg.rc) +target_sources(topgg PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/topgg.rc) 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 PRIVATE $,__TOPGG_BUILDING_DLL__,DPP_STATIC TOPGG_STATIC>) endif() if(ENABLE_CORO) @@ -34,26 +49,93 @@ else() set(TOPGG_CXX_STANDARD 17) endif() +if(TESTING) +target_compile_definitions(topgg PRIVATE __TOPGG_TESTING__) +endif() + set_target_properties(topgg PROPERTIES - OUTPUT_NAME topgg CXX_STANDARD ${TOPGG_CXX_STANDARD} CXX_STANDARD_REQUIRED ON ) -set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) -set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) +if(ENABLE_API) +set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_CURRENT_SOURCE_DIR}/cmake) find_package(DPP REQUIRED) +endif() + +if(ENABLE_CPP_HTTPLIB_WEBHOOKS) +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib/cpp-httplib.h") +execute_process(COMMAND git clone https://github.com/yhirose/cpp-httplib.git --depth 1 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib") +file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib") +endif() +file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/cpp-httplib/httplib.h" "${CMAKE_CURRENT_SOURCE_DIR}/include/cpp-httplib/httplib.h") +file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/cpp-httplib") +endif() + +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann/json.hpp") +execute_process(COMMAND git clone https://github.com/nlohmann/json.git --depth 1 WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}) +if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann") +file(MAKE_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann") +endif() +file(RENAME "${CMAKE_CURRENT_SOURCE_DIR}/json/single_include/nlohmann/json.hpp" "${CMAKE_CURRENT_SOURCE_DIR}/include/nlohmann/json.hpp") +file(REMOVE_RECURSE "${CMAKE_CURRENT_SOURCE_DIR}/json") +endif() +endif() + +if(ENABLE_DROGON_WEBHOOKS) +if(WIN32) +set(CMAKE_TOOLCHAIN_FILE "${CMAKE_CURRENT_SOURCE_DIR}/conan_toolchain.cmake") + +include(${CMAKE_CURRENT_SOURCE_DIR}/conan_toolchain.cmake) +endif() + +set(DROGON_LIBRARY drogon) + +set( + TRANTOR_INCLUDE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/trantor" + "${CMAKE_BINARY_DIR}/deps/drogon/trantor/exports" +) + +set( + DROGON_INCLUDE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/lib/inc" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/orm_lib/inc" + "${CMAKE_CURRENT_SOURCE_DIR}/deps/drogon/nosql_lib/redis/inc" + "${CMAKE_BINARY_DIR}/deps/drogon/exports" +) + +set(BUILD_CTL OFF) +set(BUILD_EXAMPLES OFF) +set(BUILD_BROTLI OFF) +set(BUILD_YAML_CONFIG OFF) +set(USE_SUBMODULE ON) + +add_subdirectory(deps/drogon) + +target_compile_definitions(topgg PUBLIC __TOPGG_DROGON_WEBHOOKS__) + +if(WIN32) +target_compile_definitions(topgg PUBLIC _CRT_SECURE_NO_WARNINGS) +cmake_policy(SET CMP0091 NEW) +endif() +endif() if(MSVC) -target_compile_options(topgg PUBLIC $<$:/diagnostics:caret /MTd> $<$:/MT /O2 /Oi /Oy /Gy>) +target_compile_options(topgg PUBLIC /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) endif() target_include_directories(topgg PUBLIC - ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/include ${DPP_INCLUDE_DIR} + ${JSONCPP_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIR} + ${TRANTOR_INCLUDE_DIR} + ${DROGON_INCLUDE_DIR} ) -target_link_libraries(topgg ${DPP_LIBRARIES}) \ No newline at end of file +target_link_libraries(topgg PUBLIC ${DPP_LIBRARIES} ${JSONCPP_LIBRARIES} ${DROGON_LIBRARY}) \ No newline at end of file diff --git a/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/README.md b/README.md index 5a1debe..edb59a2 100644 --- a/README.md +++ b/README.md @@ -1,177 +1,647 @@ -# Top.gg SDK for C++ +# Top.gg C++ SDK -The official C++ SDK for the [Top.gg API](https://docs.top.gg). +The community-maintained C++17 library for Top.gg. ## Building from source -**NOTE:** To enable C++20 coroutine methods, add `-DENABLE_CORO=ON`! +First, clone the git repository like so: -### Linux (Debian-like) +```sh +$ git clone https://github.com/Top-gg-Community/cpp-sdk --depth 1 +$ cd cpp-sdk +$ git submodule update --init --recursive +``` + +The C++ SDK provides building options that you can enable or disable by setting the corresponding variables to `ON` or `OFF`. They are as follows: + +| Option name | Description | Default | +| ----------------------------- | ------------------------------------------------ | ------- | +| `BUILD_SHARED_LIBS` | Build shared libraries. | `ON` | +| `ENABLE_API` | Build primary API support. | `ON` | +| `ENABLE_CPP_HTTPLIB_WEBHOOKS` | Build support for webhooks via `cpp-httplib`. | `OFF` | +| `ENABLE_DROGON_WEBHOOKS` | Build support for webhooks via `drogon`. | `OFF` | +| `ENABLE_CORO` | Add support for C++20 coroutines. | `OFF` | +| `TESTING` | Enable this only if you are testing the library. | `OFF` | + +### Main API wrapper + +#### Linux (Debian-like) + +Install D++: ```sh -# install D++ -wget -O dpp.deb https://dl.dpp.dev/latest -dpkg -i dpp.deb +$ wget -O dpp.deb https://dl.dpp.dev/latest +$ sudo dpkg -i dpp.deb +``` + +Build `topgg`: -# build topgg -cmake -B build . -cmake --build build --config Release +```sh +$ cmake -B build . +$ cmake --build build --config Release ``` -### Linux (CentOS-like) +#### Linux (CentOS-like) + +Install D++: ```sh -# install D++ -yum install wget -wget -O dpp.rpm https://dl.dpp.dev/latest/linux-x64/rpm -yum localinstall dpp.rpm +$ sudo yum install wget +$ wget -O dpp.rpm https://dl.dpp.dev/latest/linux-x64/rpm +$ sudo yum localinstall dpp.rpm +``` + +Build `topgg`: -# build topgg -cmake -B build . -cmake --build build --config Release +```sh +$ cmake -B build . +$ cmake --build build --config Release ``` -### macOS +#### macOS + +Install D++: ```sh -# install D++ -brew install libdpp -brew link libdpp +$ brew install libdpp +$ brew link libdpp +``` + +Build `topgg`: -# build topgg -cmake -B build . -cmake --build build --config Release +```sh +$ cmake -B build . +$ cmake --build build --config Release +``` + +#### Windows + +Install D++ and build `topgg`: + +```bat +> cmake -B build . +> cmake --build build --config Release ``` -### Windows +### Webhooks only + +#### cpp-httplib + +Install `cpp-httplib` and build `topgg`: ```bat -cmake -B build . -cmake --build build --config Release +> cmake -DENABLE_API=OFF -DENABLE_CPP_HTTPLIB_WEBHOOKS=ON -B build . +> cmake --build build --config Release +``` + +#### Drogon + +##### Linux (Debian-like) + +Install the C/C++ compiler(s): + +```sh +$ sudo apt install git gcc g++ cmake +``` + +Install Drogon's dependencies: + +```sh +$ sudo apt install libjsoncpp-dev uuid-dev zlib1g-dev ``` -## Examples +Install Drogon and build `topgg`: + +```sh +$ cmake -DENABLE_API=OFF -DENABLE_DROGON_WEBHOOKS=ON -B build . +$ cmake --build build --config Release +``` -### Fetching a bot from its Discord ID +##### Linux (Arch-like) + +Install the C/C++ compiler(s): + +```sh +$ sudo pacman -S git gcc make cmake +``` + +Install Drogon's dependencies: + +```sh +$ sudo pacman -S jsoncpp uuid zlib +``` + +Install Drogon and build `topgg`: + +```sh +$ cmake -DENABLE_API=OFF -DENABLE_DROGON_WEBHOOKS=ON -B build . +$ cmake --build build --config Release +``` + +##### Linux (CentOS-like) + +Install the C/C++ compiler(s): + +```sh +$ sudo yum install git gcc gcc-c++ +``` + +Install the latest version of CMake from source if you haven't already: + +```sh +$ git clone https://github.com/Kitware/CMake --depth 1 +$ cd CMake +$ ./bootstrap && make && make install +$ cd .. +``` + +Update your system's GCC: + +```sh +$ sudo yum install centos-release-scl devtoolset-11 +$ scl enable devtoolset-11 bash +``` + +Install Drogon's dependencies: + +```sh +$ git clone https://github.com/open-source-parsers/jsoncpp --depth 1 +$ cd jsoncpp +$ mkdir build && cd build +$ cmake .. && make && make install +$ cd ../.. +$ sudo yum install libuuid-devel zlib-devel +``` + +Install Drogon and build `topgg`: + +```sh +$ cmake -DENABLE_API=OFF -DENABLE_DROGON_WEBHOOKS=ON -B build . +$ cmake --build build --config Release +``` + +##### macOS + +Install Drogon's dependencies: + +```sh +$ brew upgrade +$ brew install jsoncpp ossp-uuid zlib-devel +``` + +Install Drogon and build `topgg`: + +```sh +$ cmake -DENABLE_API=OFF -DENABLE_DROGON_WEBHOOKS=ON -B build . +$ cmake --build build --config Release +``` + +##### Windows + +Install `conan` if you haven't already: + +```bat +> pip install conan +``` + +Install Drogon's dependencies: + +```bat +> conan profile detect --force +> conan install . -s compiler="msvc" -s compiler.version=193 -s compiler.cppstd=17 -s build_type=Release --output-folder . --build=missing -g CMakeToolchain +``` + +Install Drogon and build `topgg`: + +```bat +> cmake -DENABLE_API=OFF -DENABLE_DROGON_WEBHOOKS=ON -B build . +> cmake --build build --config Release +``` + +## Setting up ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +// If you compiled this library statically, uncomment the following line. +// #define TOPGG_STATIC + +#include +#include + +#include +#include + +int main() { + const auto discord_token{std::getenv("BOT_TOKEN")}; + const auto topgg_token{std::getenv("TOPGG_TOKEN")}; + + if (discord_token == nullptr) { + std::cerr << "error: missing BOT_TOKEN environment variable" << std::endl; + return 1; + } else if (topgg_token == nullptr) { + std::cerr << "error: missing TOPGG_TOKEN environment variable" << std::endl; + return 1; + } -// using C++17 callbacks -topgg_client.get_bot(264811613708746752, [](const auto& result) { + dpp::cluster bot{discord_token}; + topgg::client client{bot, topgg_token}; + + return 0; +} +``` + +## Usage + +### Getting a bot + +#### With C++17 callbacks + +```cpp +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; + std::cerr << "error: " << exc.what() << std::endl; } }); +``` + +#### With C++20 coroutines -// using C++20 coroutines +```cpp try { - const auto topgg_bot = co_await topgg_client.co_get_bot(264811613708746752); - + const auto topgg_bot = co_await client.co_get_bot(264811613708746752); + std::cout << topgg_bot.username << std::endl; } catch (const std::exception& exc) { - std::cout << "error: " << exc.what() << std::endl; + std::cerr << "error: " << exc.what() << std::endl; } ``` -### Fetching a user from its Discord ID +### Getting several bots + +#### With C++17 callbacks + +```cpp +client + .get_bots() + .limit(250) + .skip(50) + .sort_by_monthly_votes() + .send([](const auto& result) { + try { + const auto bots = result.get(); + + for (const auto& bot: bots) { + std::cout << bot.username << std::endl; + } + } catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; + } + }); +``` + +#### With C++20 coroutines ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +try { + const auto bots = co_await client + .get_bots() + .limit(250) + .skip(50) + .sort_by_monthly_votes() + .send(); + + for (const auto& bot: bots) { + std::cout << topgg_bot.username << std::endl; + } +} catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; +} +``` + +### Getting your bot's voters -// using C++17 callbacks -topgg_client.get_user(264811613708746752, [](const auto& result) { +#### With C++17 callbacks + +```cpp +// First page +client.get_voters([](const auto& result) { try { - const auto user = result.get(); - - std::cout << user.username << std::endl; + 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; + std::cerr << "error: " << exc.what() << std::endl; } }); -// using C++20 coroutines +// Subsequent pages +client.get_voters(2, [](const auto& result) { + try { + auto voters = result.get(); + + for (auto& voter: voters) { + std::cout << voter.username << std::endl; + } + } catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; + } +}); +``` + +#### With C++20 coroutines + +```cpp +// First page try { - const auto user = co_await topgg_client.co_get_user(661200758510977084); - - std::cout << user.username << std::endl; + const auto voters = co_await client.co_get_voters(); + + for (const auto& voter: voters) { + std::cout << voter.username << std::endl; + } +} catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; +} + +// Subsequent pages +try { + const auto voters = co_await client.co_get_voters(2); + + for (const auto& voter: voters) { + std::cout << voter.username << std::endl; + } +} catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; +} +``` + +### Check if a user has voted for your bot + +#### With C++17 callbacks + +```cpp +client.has_voted(661200758510977084, [](const auto& result) { + try { + if (result.get()) { + std::cout << "Checks out." << std::endl; + } + } catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; + } +}); +``` + +#### With C++20 coroutines + +```cpp +try { + const auto voted = co_await 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; + std::cerr << "error: " << exc.what() << std::endl; } ``` -### Posting your bot's statistics +### Getting your bot's server count + +#### With C++17 callbacks ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +client.get_server_count([](const auto& result) { + try { + auto server_count = result.get(); + + std::cout << server_count.value_or(0) << std::endl; + } catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; + } +}); +``` -// using C++17 callbacks -topgg_client.post_stats([](const auto success) { +#### With C++20 coroutines + +```cpp +try { + const auto server_count = co_await client.co_get_server_count(); + + std::cout << server_count.value_or(0) << std::endl; +} catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; +} +``` + +### Posting your bot's server count + +#### With C++17 callbacks + +```cpp +client.post_server_count([](const auto success) { if (success) { - std::cout << "stats posted!" << std::endl; + std::cout << "Stats posted!" << std::endl; } }); +``` -// using C++20 coroutines -const auto success = co_await topgg_client.co_post_stats(); +#### With C++20 coroutines + +```cpp +const auto success = co_await client.co_post_server_count(); if (success) { - std::cout << "stats posted!" << std::endl; + std::cout << "Stats posted!" << std::endl; } ``` -### Checking if a user has voted your bot +### Automatically posting your bot's server count every few minutes + +#### Without a callback ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +client.start_autoposter(); +``` -// using C++17 callbacks -topgg_client.has_voted(661200758510977084, [](const auto& result) { +#### With a callback + +```cpp +client.start_autoposter([](const auto& result) { + if (result) { + std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; + } +}); +``` + +#### From a custom source + +```cpp +class my_autoposter_source: private topgg::autoposter_source { +public: + virtual size_t get_server_count(dpp::cluster& bot) { + return ...; + } +}; + +client.start_autoposter(reinterpret_cast(new my_autoposter_source), [](const auto& result) { + if (result) { + std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; + } +}); +``` + +### Checking if the weekend vote multiplier is active + +#### With C++17 callbacks + +```cpp +client.is_weekend([](const auto& result) { try { if (result.get()) { - std::cout << "checks out" << std::endl; + std::cout << "The weekend multiplier is active" << std::endl; } } catch (const std::exception& exc) { - std::cout << "error: " << exc.what() << std::endl; + std::cerr << "error: " << exc.what() << std::endl; } }); +``` + +#### With C++20 coroutines -// using C++20 coroutines +```cpp try { - const auto voted = co_await topgg_client.co_has_voted(661200758510977084); + const auto is_weekend = co_await client.co_is_weekend(); - if (voted) { - std::cout << "checks out" << std::endl; + if (is_weekend) { + std::cout << "The weekend multiplier is active" << std::endl; } } catch (const std::exception& exc) { - std::cout << "error: " << exc.what() << std::endl; + std::cerr << "error: " << exc.what() << std::endl; } ``` -### Default autoposting +### Generating widget URLs + +#### Large ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +const auto widget_url{topgg::widget::large(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; +``` -topgg_client.start_autoposter(); +#### Votes + +```cpp +const auto widget_url{topgg::widget::votes(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; ``` -### Customized autoposting +#### Owner ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +const auto widget_url{topgg::widget::owner(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; +``` -topgg_client.start_autoposter([](dpp::cluster& bot_inner) { - return topgg::stats{...}; -}); +#### Social + +```cpp +const auto widget_url{topgg::widget::social(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; +``` + +### Webhooks + +#### Being notified whenever someone voted for your bot + +##### cpp-httplib + +```cpp +// If you compiled this library statically, uncomment the following line. +// #define TOPGG_STATIC + +#include +#include + +#include +#include + +template +using cpp_httplib_webhook = topgg::webhook::cpp_httplib; +using topgg::webhook::vote; + +class my_vote_listener: public cpp_httplib_webhook { +public: + inline my_vote_listener(const std::string& authorization): cpp_httplib_webhook(authorization) {} + + void callback(const vote& v) override { + std::cout << "A user with the ID of " << v.voter_id << " has voted us on Top.gg!" << std::endl; + } +}; + +int main() { + const auto authorization{std::getenv("MY_TOPGG_WEBHOOK_SECRET")}; + + if (authorization == nullptr) { + std::cerr << "error: missing MY_TOPGG_WEBHOOK_SECRET environment variable" << std::endl; + return 1; + } + + httplib::Server server{}; + my_vote_listener webhook{authorization}; + + server.Post("/votes", webhook.endpoint()); + server.listen("localhost", 8080); + + return 0; +} +``` + +##### Drogon + +```cpp +// If you compiled this library statically, uncomment the following line. +// #define TOPGG_STATIC + +#include + +#include +#include +#include + +template +using drogon_webhook = topgg::webhook::drogon; +using topgg::webhook::vote; + +class my_vote_listener: public ::drogon::HttpSimpleController, public drogon_webhook { +public: + inline my_vote_listener(const std::string& authorization): drogon_webhook(authorization) {} + + TOPGG_DROGON_WEBHOOK(); + + PATH_LIST_BEGIN + PATH_ADD("/votes", ::drogon::Post); + PATH_LIST_END + + void callback(const vote& v) override { + std::cout << "A user with the ID of " << v.voter_id << " has voted us on Top.gg!" << std::endl; + } +}; + +int main() { + const auto authorization{std::getenv("MY_TOPGG_WEBHOOK_SECRET")}; + + if (authorization == nullptr) { + std::cerr << "error: missing MY_TOPGG_WEBHOOK_SECRET environment variable" << std::endl; + return 1; + } + + auto& app{drogon::app()}; + + app.registerController(std::make_shared(authorization)); + app.addListener("127.0.0.1", 8080); + app.run(); + + return 0; +} ``` \ No newline at end of file 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/conanfile.txt b/conanfile.txt new file mode 100644 index 0000000..5d648d2 --- /dev/null +++ b/conanfile.txt @@ -0,0 +1,3 @@ +[requires] +jsoncpp/1.9.4 +zlib/1.2.11 diff --git a/deps/drogon b/deps/drogon new file mode 160000 index 0000000..8079e76 --- /dev/null +++ b/deps/drogon @@ -0,0 +1 @@ +Subproject commit 8079e76aef32d6ea8b74887490dbea246731ddaf diff --git a/docs/Doxyfile b/docs/Doxyfile index b18a078..aa24b09 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 = 3.0.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 simple API wrapper for Top.gg written in C++." # 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..e6a59e7 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..3abca0a 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 simple API wrapper for Top.gg written in C++. * @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-07-02 + * @version 3.0.0 */ #pragma once @@ -24,23 +24,15 @@ namespace topgg { * @see topgg::client::get_bot * @since 2.0.0 */ - using get_bot_completion_t = std::function&)>; + using get_bot_completion_event = std::function&)>; /** - * @brief The callback function to call when get_user completes. + * @brief The callback function to call when get_server_count completes. * - * @see topgg::client::get_user - * @since 2.0.0 - */ - using get_user_completion_t = std::function&)>; - - /** - * @brief The callback function to call when get_stats completes. - * - * @see topgg::client::get_stats - * @since 2.0.0 + * @see topgg::client::get_server_count + * @since 3.0.0 */ - using get_stats_completion_t = std::function&)>; + using get_server_count_completion_event = std::function>&)>; /** * @brief The callback function to call when get_voters completes. @@ -48,7 +40,7 @@ namespace topgg { * @see topgg::client::get_voters * @since 2.0.0 */ - using get_voters_completion_t = std::function>&)>; + using get_voters_completion_event = std::function>&)>; /** * @brief The callback function to call when has_voted completes. @@ -56,7 +48,7 @@ namespace topgg { * @see topgg::client::has_voted * @since 2.0.0 */ - using has_voted_completion_t = std::function&)>; + using has_voted_completion_event = std::function&)>; /** * @brief The callback function to call when is_weekend completes. @@ -64,40 +56,45 @@ namespace topgg { * @see topgg::client::is_weekend * @since 2.0.0 */ - using is_weekend_completion_t = std::function&)>; - + using is_weekend_completion_event = std::function&)>; + /** - * @brief The callback function to call when post_stats completes. + * @brief The callback function to call when post_server_count completes. * - * @see topgg::client::post_stats - * @since 2.0.0 + * @see topgg::client::post_server_count + * @since 3.0.0 */ - using post_stats_completion_t = std::function; - + using post_server_count_completion_event = 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 - * @since 2.0.0 + * @see topgg::client::stop_autoposter + * @since 3.0.0 */ - using custom_autopost_callback_t = std::function<::topgg::stats(dpp::cluster&)>; - + using autopost_completion_event = std::function&)>; + /** - * @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; + std::string 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); + m_cluster.request(TOPGG_BASE_URL + 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); } - + + size_t get_server_count(); + void post_server_count_inner(const size_t server_count, dpp::http_completion_event callback); + public: client() = delete; @@ -105,7 +102,7 @@ namespace topgg { * @brief Constructs the client class. * * @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. To retrieve it, see https://github.com/top-gg/rust-sdk/assets/60427892/d2df5bd3-bc48-464c-b878-a04121727bff. * @since 2.0.0 */ client(dpp::cluster& cluster, const std::string& token); @@ -145,26 +142,26 @@ namespace topgg { client& operator=(client&& other) = delete; /** - * @brief Fetches a listed Discord bot from a Discord ID. + * @brief Fetches a Discord bot from its ID. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * topgg_client.get_bot(264811613708746752, [](const auto& result) { + * 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; + * std::cerr << "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 @@ -172,33 +169,33 @@ namespace topgg { * @see topgg::client::co_get_bot * @since 2.0.0 */ - void get_bot(const dpp::snowflake bot_id, const get_bot_completion_t& callback); + void get_bot(const dpp::snowflake bot_id, const get_bot_completion_event& callback); #ifdef DPP_CORO /** - * @brief Fetches a listed Discord bot from a Discord ID through a C++20 coroutine. + * @brief Fetches a Discord bot from its ID through a C++20 coroutine. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto topgg_bot = co_await topgg_client.co_get_bot(264811613708746752); + * const auto topgg_bot = co_await client.co_get_bot(264811613708746752); * * std::cout << topgg_bot.username << std::endl; * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "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. + * @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,143 +207,134 @@ namespace topgg { #endif /** - * @brief Fetches a user from a Discord ID. + * @brief Returns an object that allows you to configure a bot query before sending it to the API. * - * Example: + * C++17 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; - * } - * }); + * topgg::client client{bot, "your top.gg token"}; + * + * client + * .get_bots() + * .limit(250) + * .skip(50) + * .sort_by_monthly_votes() + * .send([](const auto& result) { + * try { + * const auto bots = result.get(); + * + * for (const auto& bot: bots) { + * std::cout << bot.username << std::endl; + * } + * } catch (const std::exception& exc) { + * std::cerr << "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. - * @see topgg::result - * @see topgg::user - * @see topgg::co_get_user - * @since 2.0.0 - */ - void get_user(const dpp::snowflake user_id, const get_user_completion_t& callback); - -#ifdef DPP_CORO - /** - * @brief Fetches a user from a Discord ID through a C++20 coroutine. - * - * Example: + * C++20 example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto user = co_await topgg_client.co_get_user(661200758510977084); - * - * std::cout << user.username << std::endl; + * const auto bots = co_await client + * .get_bots() + * .limit(250) + * .skip(50) + * .sort_by_monthly_votes() + * .send(); + * + * for (const auto& bot: bots) { + * std::cout << topgg_bot.username << std::endl; + * } * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "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. - * @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::user if successful - * @note For its C++17 callback-based counterpart, see get_user. - * @see topgg::async_result - * @see topgg::bot - * @see topgg::client::get_user - * @since 2.0.0 + * @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 */ - topgg::async_result co_get_user(const dpp::snowflake user_id); -#endif + inline bot_query get_bots() noexcept { + return bot_query{this}; + } /** - * @brief Fetches your Discord bot’s statistics. + * @brief Fetches your Discord bot's posted server count. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * topgg_client.get_stats([](const auto& result) { + * client.get_server_count([](const auto& result) { * try { - * auto stats = result.get(); + * auto server_count = result.get(); * - * std::cout << stats.server_count().value_or(0) << std::endl; + * std::cout << server_count.value_or(0) << std::endl; * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "error: " << exc.what() << std::endl; * } * }); * ``` * - * @param callback The callback function to call when get_stats completes. - * @note For its C++20 coroutine counterpart, see co_get_stats. + * @param callback The callback function to call when get_server_count completes. + * @note For its C++20 coroutine counterpart, see co_get_server_count. * @see topgg::result * @see topgg::client::start_autoposter - * @see topgg::client::co_get_stats - * @since 2.0.0 + * @see topgg::client::co_get_server_count + * @since 3.0.0 */ - void get_stats(const get_stats_completion_t& callback); + void get_server_count(const get_server_count_completion_event& callback); #ifdef DPP_CORO /** - * @brief Fetches your Discord bot’s statistics through a C++20 coroutine. + * @brief Fetches your Discord bot's posted server count through a C++20 coroutine. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto stats = co_await topgg_client.co_get_stats(); + * const auto server_count = co_await client.co_get_server_count(); * - * std::cout << stats.server_count().value_or(0) << std::endl; + * std::cout << server_count.value_or(0) << std::endl; * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "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 - * @note For its C++17 callback-based counterpart, see get_stats. + * @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_server_count. * @see topgg::async_result * @see topgg::client::start_autoposter - * @see topgg::client::get_stats - * @since 2.0.0 + * @see topgg::client::get_server_count + * @since 3.0.0 */ - topgg::async_result co_get_stats(); + topgg::async_result> co_get_server_count(); #endif /** - * @brief Fetches your Discord bot’s last 1000 voters. + * @brief Fetches your Discord bot's recent 100 unique voters. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * topgg_client.get_voters([](const auto& result) { + * client.get_voters(1, [](const auto& result) { * try { * auto voters = result.get(); * @@ -354,122 +342,151 @@ namespace topgg { * std::cout << voter.username << std::endl; * } * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "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(const get_voters_completion_t& callback); + void get_voters(size_t page, const get_voters_completion_event& callback); + + /** + * @brief Fetches your Discord bot's recent 100 unique voters. + * + * Example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client client{bot, "your top.gg token"}; + * + * 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::cerr << "error: " << exc.what() << std::endl; + * } + * }); + * ``` + * + * @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::client::start_autoposter + * @see topgg::client::co_get_voters + * @since 2.0.0 + */ + void get_voters(const get_voters_completion_event& callback); #ifdef DPP_CORO /** - * @brief Fetches your Discord bot’s last 1000 voters through a C++20 coroutine. + * @brief Fetches your Discord bot's recent 100 unique voters through a C++20 coroutine. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto voters = co_await topgg_client.co_get_voters(); + * const auto voters = co_await 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; + * std::cerr << "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 + * @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 - * @see topgg::stats * @see topgg::client::start_autoposter * @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. + * @brief Checks if the specified Discord user has voted your Discord bot. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * topgg_client.has_voted(661200758510977084, [](const auto& result) { + * client.has_voted(661200758510977084, [](const auto& result) { * try { * if (result.get()) { - * std::cout << "checks out" << std::endl; + * std::cout << "Checks out." << std::endl; * } * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "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); + void has_voted(const dpp::snowflake user_id, const has_voted_completion_event& callback); #ifdef DPP_CORO /** - * @brief Checks if the specified user has voted your Discord bot through a C++20 coroutine. + * @brief Checks if the specified Discord 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"}; + * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto voted = co_await topgg_client.co_has_voted(661200758510977084); + * const auto voted = co_await client.co_has_voted(661200758510977084); * * if (voted) { - * std::cout << "checks out" << std::endl; + * std::cout << "Checks out." << std::endl; * } * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "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. + * @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 - * @see topgg::stats * @see topgg::client::start_autoposter * @see topgg::client::has_voted * @since 2.0.0 @@ -478,21 +495,21 @@ namespace topgg { #endif /** - * @brief Checks if the weekend multiplier is active. + * @brief Checks if the weekend multiplier is active, where a single vote counts as two. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * topgg_client.is_weekend([](const auto& result) { + * client.is_weekend([](const auto& result) { * try { * if (result.get()) { - * std::cout << "the weekend multiplier is active" << std::endl; + * std::cout << "The weekend multiplier is active" << std::endl; * } * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "error: " << exc.what() << std::endl; * } * }); * ``` @@ -503,34 +520,33 @@ namespace topgg { * @see topgg::client::co_is_weekend * @since 2.0.0 */ - void is_weekend(const is_weekend_completion_t& callback); + void is_weekend(const is_weekend_completion_event& callback); #ifdef DPP_CORO /** - * @brief Checks if the weekend multiplier is active through a C++20 coroutine. + * @brief Checks if the weekend multiplier is active through a C++20 coroutine, where a single vote counts as two. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto is_weekend = co_await topgg_client.co_is_weekend(); + * const auto is_weekend = co_await client.co_is_weekend(); * * if (is_weekend) { - * std::cout << "the weekend multiplier is active" << std::endl; + * std::cout << "The weekend multiplier is active" << std::endl; * } * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "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. + * @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,185 +557,193 @@ namespace topgg { #endif /** - * @brief Manually posts your Discord bot's statistics using data directly from your D++ cluster instance. + * @brief Posts your Discord bot's server count to the API. This will update the server count in your bot's Top.gg page. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * topgg_client.post_stats([](const auto success) { + * client.post_server_count([](const auto success) { * if (success) { - * std::cout << "stats posted!" << std::endl; + * std::cout << "Stats posted!" << std::endl; * } * }); * ``` * - * @param callback The callback function to call when post_stats completes. - * @note For its C++20 coroutine counterpart, see co_post_stats. + * @param callback The callback function to call when post_server_count completes. + * @note For its C++20 coroutine counterpart, see co_post_server_count. * @see topgg::result * @see topgg::client::start_autoposter - * @see topgg::client::co_post_stats - * @since 2.0.0 + * @see topgg::client::co_post_server_count + * @since 3.0.0 */ - void post_stats(const post_stats_completion_t& callback); + void post_server_count(const post_server_count_completion_event& callback); #ifdef DPP_CORO /** - * @brief Manually posts your Discord bot's statistics using data directly from your D++ cluster instance through a C++20 coroutine. + * @brief Posts your Discord bot's server count to the API through a C++20 coroutine. This will update the server count in your bot's Top.gg page. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * const auto success = co_await topgg_client.co_post_stats(); + * const auto success = co_await client.co_post_server_count(); * * if (success) { - * std::cout << "stats posted!" << std::endl; + * std::cout << "Stats posted!" << std::endl; * } * ``` * * @return co_await to retrieve a bool - * @note For its C++17 callback-based counterpart, see post_stats. + * @note For its C++17 callback-based counterpart, see post_server_count. * @see topgg::client::start_autoposter - * @see topgg::client::post_stats - * @since 2.0.0 + * @see topgg::client::post_server_count + * @since 3.0.0 */ - dpp::async co_post_stats(); + dpp::async co_post_server_count(); #endif /** - * @brief Manually posts your Discord bot's statistics. + * @brief Starts autoposting your Discord bot's server count 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"}; - * - * const size_t server_count = 12345; + * topgg::client client{bot, "your top.gg token"}; * - * topgg_client.post_stats(topgg::stats{server_count}, [](const auto success) { - * if (success) { - * std::cout << "stats posted!" << std::endl; + * client.start_autoposter([](const auto& result) { + * if (result) { + * std::cout << "Successfully posted " << *result << " servers to the API!" << 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_server_count + * @see topgg::client::stop_autoposter + * @see topgg::autopost_completion_event * @since 2.0.0 */ - void post_stats(const stats& s, const post_stats_completion_t& callback); + void start_autoposter(const autopost_completion_event& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); -#ifdef DPP_CORO /** - * @brief Manually posts your Discord bot's statistics through a C++20 coroutine. + * @brief Starts autoposting your Discord bot's server count 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 client{bot, "your top.gg token"}; * - * 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; - * } + * client.start_autoposter(); * ``` * - * @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 - * @see topgg::client::post_stats + * @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_server_count + * @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 server count using a custom data source. * * Example: * * ```cpp + * class my_autoposter_source: private topgg::autoposter_source { + * public: + * virtual size_t get_server_count(dpp::cluster& bot) { + * return ...; + * } + * }; + * * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * bot.start_autoposter(); + * client.start_autoposter(reinterpret_cast(new my_autoposter_source), [](const auto& result) { + * if (result) { + * std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; + * } + * }); * ``` * - * @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 an autoposter 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::post_server_count * @see topgg::client::stop_autoposter + * @see topgg::autopost_completion_event + * @see topgg::autoposter_source * @since 2.0.0 */ - void start_autoposter(const time_t delay = 1800); - + void start_autoposter(autoposter_source* source, const autopost_completion_event& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); + /** - * @brief Starts autoposting statistics. + * @brief Starts autoposting your Discord bot's server count using a custom data source. * * Example: * * ```cpp + * class my_autoposter_source: private topgg::autoposter_source { + * public: + * virtual size_t get_server_count(dpp::cluster& bot) { + * return ...; + * } + * }; + * * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * bot.start_autoposter([](dpp::cluster& bot_inner) { - * return topgg::stats{...}; - * }); + * client.start_autoposter(reinterpret_cast(new my_autoposter_source)); * ``` * - * @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 an autoposter 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::post_server_count * @see topgg::client::stop_autoposter - * @since 2.0.0 + * @see topgg::autoposter_source + * @since 3.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. + * @brief Prematurely stops the autoposter. Calling this function is usually unnecessary as this function will be called in the destructor. * * Example: * * ```cpp * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; + * topgg::client client{bot, "your top.gg token"}; * - * bot.start_autoposter(); + * client.start_autoposter(); * * // ... * - * bot.stop_autoposter(); + * client.stop_autoposter(); * ``` * * @note This function has no effect if the autoposter is already stopped. - * @see topgg::client::post_stats + * @see topgg::client::post_server_count * @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..cb6ab47 --- /dev/null +++ b/include/topgg/export.h @@ -0,0 +1,38 @@ +/** + * @module topgg + * @file export.h + * @brief A simple API wrapper for Top.gg written in C++. + * @authors Top.gg, null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-07-02 + * @version 3.0.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..2ccbb84 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 simple API wrapper for Top.gg written in C++. * @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-07-02 + * @version 3.0.0 */ #pragma once #include -#include +#include #include #include #include @@ -27,429 +27,467 @@ #undef _XOPEN_SOURCE #endif +#define TOPGG_WIDGET_DISCORD_BOT "discord/bot" +#define TOPGG_WIDGET_DISCORD_SERVER "discord/server" + +#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 voter. * - * @see topgg::bot - * @see topgg::user - * @see topgg::voter + * @see topgg::client::get_voters + * @see topgg::client::start_autoposter * @since 2.0.0 */ - class TOPGG_EXPORT account { - protected: - account(const dpp::json& j); + class TOPGG_EXPORT voter { + voter(const dpp::json& j); public: - account() = delete; + voter() = delete; /** - * @brief The account's Discord ID. + * @brief This voter's Discord ID. * * @since 2.0.0 */ dpp::snowflake id; /** - * @brief The account's entire Discord avatar URL. + * @brief This voter'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 voter's username. * - * @since 2.0.0 + * @since 3.0.0 */ std::string username; /** - * @brief The unix timestamp of when this account was created. + * @brief This voter's creation date. * * @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) {} - - public: - voter() = delete; friend class client; }; /** - * @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 * @since 2.0.0 */ - class TOPGG_EXPORT bot: public account { + class TOPGG_EXPORT bot { bot(const dpp::json& j); public: bot() = delete; /** - * @brief The Discord bot's discriminator. + * @brief This bot's Discord ID. * * @since 2.0.0 */ - std::string discriminator; + dpp::snowflake id; /** - * @brief The Discord bot's command prefix. + * @brief This bot's Top.gg ID. * - * @since 2.0.0 + * @since 3.0.0 */ - std::string prefix; + dpp::snowflake topgg_id; /** - * @brief The Discord bot's short description. + * @brief This bot's avatar URL. * * @since 2.0.0 */ - std::string short_description; + std::string avatar; /** - * @brief The Discord bot's long description, if available. + * @brief This bot's username. * - * @note This long description can contain Markdown and/or HTML. - * @since 2.0.0 + * @since 3.0.0 */ - std::optional long_description; + std::string username; /** - * @brief A list of the Discord bot's tags. + * @brief This bot's creation date. * * @since 2.0.0 */ - std::vector tags; + time_t created_at; /** - * @brief A link to the Discord bot's website, if available. + * @brief This bot's prefix. * * @since 2.0.0 */ - std::optional website; + std::string prefix; /** - * @brief A link to the Discord bot's GitHub repository, if available. + * @brief This bot's short description. * * @since 2.0.0 */ - std::optional github; + std::string short_description; /** - * @brief A list of the Discord bot's owners, represented in Discord user IDs. + * @brief This bot's long description. * + * @note This long description can contain Markdown and/or HTML. * @since 2.0.0 */ - std::vector owners; + std::optional long_description; /** - * @brief A list of IDs of the guilds featured on this Discord bot’s page. + * @brief This bot's tags. * * @since 2.0.0 */ - std::vector guilds; + std::vector tags; /** - * @brief The Discord bot's page banner URL, if available. + * @brief This bot's website URL. * * @since 2.0.0 */ - std::optional banner; + std::optional website; /** - * @brief The unix timestamp of when this Discord bot was approved on Top.gg by a Bot Reviewer. + * @brief This bot's GitHub repository URL. * * @since 2.0.0 */ - time_t approved_at; + std::optional github; /** - * @brief Whether this Discord bot is Top.gg certified or not. + * @brief This bot's owner IDs. * * @since 2.0.0 */ - bool is_certified; + std::vector owners; /** - * @brief A list of this Discord bot’s shards. + * @brief This bot's submission date. * - * @since 2.0.0 + * @since 3.0.0 */ - std::vector shards; + time_t submitted_at; /** - * @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 */ std::optional support; /** - * @brief The amount of shards this Discord bot has according to posted stats. + * @brief This bot's invite URL. * * @since 2.0.0 */ - size_t shard_count; + std::optional invite; /** - * @brief The invite URL of this Discord bot. + * @brief This bot's Top.gg vanity code. * - * @since 2.0.0 + * @since 3.0.0 */ - std::string invite; + std::optional vanity; /** - * @brief The URL of this Discord bot’s Top.gg page. + * @brief This bot's posted server count. * - * @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); - - std::optional m_shard_count; - std::optional> m_shards; - std::optional m_shard_id; - std::optional m_server_count; - - std::string to_json() const; - - public: - stats() = delete; - - /** - * @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 + * @since 3.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}) {} + std::optional server_count; /** - * @brief Creates a stats object based on the bot's shard data. + * @brief This bot's average review score out of 5. * - * @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 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 + * @since 3.0.0 */ - size_t shard_count() const noexcept; + double review_score; /** - * @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. + * @brief This bot's review count. * - * @param new_server_count The new server count. - * @since 2.0.0 + * @since 3.0.0 */ - inline void set_server_count(const size_t new_server_count) noexcept { - m_server_count = std::optional{new_server_count}; - } + size_t review_count; + friend class bot_query; friend class client; }; - class user; + /** + * @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_event = std::function>&)>; /** - * @brief Represents a user's social links, if available. + * @brief Configure a Discord bot query before sending it to the API. * - * @see topgg::user - * @since 2.0.0 + * @see topgg::client::get_bots + * @since 2.0.1 */ - class TOPGG_EXPORT user_socials { - user_socials(const dpp::json& j); + 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: - user_socials() = delete; + bot_query() = delete; /** - * @brief A URL of this user’s GitHub account, if available. + * @brief Sorts results based on each bot's ID. * - * @since 2.0.0 + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 */ - std::optional github; + TOPGG_BOT_QUERY_SORT(id, id); /** - * @brief A URL of this user’s Instagram account, if available. + * @brief Sorts results based on each bot's submission date. * - * @since 2.0.0 + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 3.0.0 */ - std::optional instagram; + TOPGG_BOT_QUERY_SORT(submission_date, date); /** - * @brief A URL of this user’s Reddit account, if available. + * @brief Sorts results based on each bot's monthly vote count. * - * @since 2.0.0 + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 */ - std::optional reddit; + TOPGG_BOT_QUERY_SORT(monthly_votes, monthlyPoints); /** - * @brief A URL of this user’s Twitter/X account, if available. + * @brief Sets the maximum amount of bots to be queried. * - * @since 2.0.0 + * @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 */ - std::optional twitter; + TOPGG_BOT_QUERY_QUERY(uint16_t, limit, limit, 500); /** - * @brief A URL of this user’s YouTube channel, if available. + * @brief Sets the amount of bots to be skipped. * - * @since 2.0.0 + * @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 */ - 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 { - user(const dpp::json& j); - - public: - user() = delete; + TOPGG_BOT_QUERY_QUERY(uint16_t, skip, offset, 499); /** - * @brief The user's bio, if available. + * @brief Sends the query to the API. * - * @since 2.0.0 - */ - std::optional bio; - - /** - * @brief The URL of this user’s profile banner image, if available. + * Example: * - * @since 2.0.0 - */ - std::optional banner; - - /** - * @brief This user's social links, if available. + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client client{bot, "your top.gg token"}; * - * @since 2.0.0 - */ - std::optional socials; - - /** - * @brief Whether this user is a Top.gg supporter or not. + * client + * .get_bots() + * .limit(250) + * .skip(50) + * .sort_by_monthly_votes() + * .send([](const auto& result) { + * try { + * const auto bots = result.get(); * - * @since 2.0.0 - */ - bool is_supporter; - - /** - * @brief Whether this user is a Top.gg certified developer or not. + * for (const auto& bot: bots) { + * std::cout << bot.username << std::endl; + * } + * } catch (const std::exception& exc) { + * std::cerr << "error: " << exc.what() << std::endl; + * } + * }); + * ``` * - * @since 2.0.0 + * @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 */ - bool is_certified_dev; + void send(const get_bots_completion_event& callback); +#ifdef DPP_CORO /** - * @brief Whether this user is a Top.gg moderator or not. + * @brief Sends the query to the API through a C++20 coroutine. * - * @since 2.0.0 - */ - bool is_moderator; - - /** - * @brief Whether this user is a Top.gg website moderator or not. + * Example: * - * @since 2.0.0 - */ - bool is_web_moderator; - - /** - * @brief Whether this user is a Top.gg website administrator or not. + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client client{bot, "your top.gg token"}; * - * @since 2.0.0 + * try { + * const auto bots = co_await client + * .get_bots() + * .limit(250) + * .skip(50) + * .sort_by_monthly_votes() + * .send(); + * + * for (const auto& bot: bots) { + * std::cout << topgg_bot.username << std::endl; + * } + * } catch (const std::exception& exc) { + * std::cerr << "error: " << exc.what() << std::endl; + * } + * ``` + * + * @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 */ - bool is_admin; + topgg::async_result> co_send(); +#endif friend class client; }; -}; // namespace topgg \ No newline at end of file + + /** + * @brief An abstract interface for bots that have a custom way of retrieving their server count. + * + * @see topgg::start_autoposter + * @since 3.0.0 + */ + class autoposter_source { + public: + virtual size_t TOPGG_EXPORT get_server_count(dpp::cluster&) = 0; + }; + + namespace widget { + /** + * @brief Generates a large widget URL. + * + * Example: + * + * ```cpp + * const auto widget_url{topgg::widget::large(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; + * + * std::cout << widget_url << std::endl; + * ``` + * + * @param ty The widget type. This can be TOPGG_WIDGET_DISCORD_BOT or TOPGG_WIDGET_DISCORD_SERVER. + * @param id The ID. + * @since 3.0.0 + */ + std::string large(const char* ty, const dpp::snowflake id); + + /** + * @brief Generates a small widget URL for displaying votes. + * + * Example: + * + * ```cpp + * const auto widget_url{topgg::widget::votes(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; + * + * std::cout << widget_url << std::endl; + * ``` + * + * @param ty The widget type. This can be TOPGG_WIDGET_DISCORD_BOT or TOPGG_WIDGET_DISCORD_SERVER. + * @param id The ID. + * @since 3.0.0 + */ + std::string votes(const char* ty, const dpp::snowflake id); + + /** + * @brief Generates a small widget URL for displaying an entity's owner. + * + * Example: + * + * ```cpp + * const auto widget_url{topgg::widget::owner(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; + * + * std::cout << widget_url << std::endl; + * ``` + * + * @param ty The widget type. This can be TOPGG_WIDGET_DISCORD_BOT or TOPGG_WIDGET_DISCORD_SERVER. + * @param id The ID. + * @since 3.0.0 + */ + std::string owner(const char* ty, const dpp::snowflake id); + + /** + * @brief Generates a small widget URL for displaying social stats. + * + * Example: + * + * ```cpp + * const auto widget_url{topgg::widget::social(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; + * + * std::cout << widget_url << std::endl; + * ``` + * + * @param ty The widget type. This can be TOPGG_WIDGET_DISCORD_BOT or TOPGG_WIDGET_DISCORD_SERVER. + * @param id The ID. + * @since 3.0.0 + */ + std::string social(const char* ty, const dpp::snowflake id); + }; // namespace widget +}; // 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..dc8796a 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 simple API wrapper for Top.gg written in C++. * @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-07-02 + * @version 3.0.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,19 @@ 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 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 +57,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,21 +67,21 @@ 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; @@ -95,18 +95,17 @@ namespace topgg { 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. + * @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; @@ -117,14 +116,14 @@ namespace topgg { 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 { @@ -138,8 +137,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 +145,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 +177,7 @@ namespace topgg { * @since 2.0.0 */ async_result& operator=(const async_result& other) = delete; - + /** * @brief Moves data from another object. * @@ -187,55 +186,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..6cc4ecc 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 simple API wrapper for Top.gg written in C++. * @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-07-02 + * @version 3.0.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 5 #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/v1" + #include #include #include \ No newline at end of file diff --git a/include/topgg/webhooks/cpp-httplib.h b/include/topgg/webhooks/cpp-httplib.h new file mode 100644 index 0000000..c223ce4 --- /dev/null +++ b/include/topgg/webhooks/cpp-httplib.h @@ -0,0 +1,82 @@ +/** + * @module topgg + * @file cpp-httplib.h + * @brief A simple API wrapper for Top.gg written in C++. + * @authors Top.gg, null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-07-02 + * @version 3.0.0 + */ + +#pragma once + +#ifndef CPPHTTPLIB_HTTPLIB_H +#include +#endif + +#include + +#include +#include +#include +#include + +namespace topgg { + namespace webhook { + class internal_cpp_httplib { + protected: + const std::string m_authorization; + + std::optional parse(const httplib::Request& request, httplib::Response& response) const noexcept; + + inline internal_cpp_httplib(const std::string& authorization): m_authorization(authorization) {} + + public: + internal_cpp_httplib() = delete; + }; + + using cpp_httplib_endpoint = std::function; + + /** + * @brief An abstract class that receives a Top.gg webhook event. Designed for use in cpp-httplib. + * + * @see topgg::webhook::vote + * @since 3.0.0 + */ + template + class cpp_httplib: private internal_cpp_httplib { + public: + cpp_httplib() = delete; + + inline cpp_httplib(const std::string& authorization): internal_cpp_httplib(authorization) { + static_assert(std::is_constructible_v, "T must be a valid model class."); + } + + /** + * @brief Returns the endpoint callback to be used in a cpp-httplib server route. + * + * @return cpp_httplib_endpoint The endpoint callback. + * @since 3.0.0 + */ + inline cpp_httplib_endpoint endpoint() noexcept { + return [this](const httplib::Request& request, httplib::Response& response) { + const auto json_data{this->parse(request, response)}; + + if (json_data.has_value()) { + const T data{json_data.value()}; + + this->callback(data); + } + }; + } + + /** + * @brief The virtual callback that will be called whenever an incoming webhook request to the endpoint has been authenticated. + * + * @param T data The webhook event data. + * @since 3.0.0 + */ + virtual void callback(const T& data) = 0; + }; + }; // namespace webhook +}; // namespace topgg \ No newline at end of file diff --git a/include/topgg/webhooks/drogon.h b/include/topgg/webhooks/drogon.h new file mode 100644 index 0000000..fff35a8 --- /dev/null +++ b/include/topgg/webhooks/drogon.h @@ -0,0 +1,84 @@ +/** + * @module topgg + * @file drogon.h + * @brief A simple API wrapper for Top.gg written in C++. + * @authors Top.gg, null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-07-02 + * @version 3.0.0 + */ + +#pragma once + +#ifndef __TOPGG_DROGON_WEBHOOKS__ +#define __TOPGG_DROGON_WEBHOOKS__ +#endif + +#include + +#include + +#include +#include +#include +#include + +#define TOPGG_DROGON_WEBHOOK() \ + void asyncHandleHttpRequest(const ::drogon::HttpRequestPtr& request, std::function&& response) override { \ + __handle(request, std::forward>(response)); \ + } + +namespace topgg { + namespace webhook { + class internal_drogon { + protected: + const std::string m_authorization; + + std::optional parse(const ::drogon::HttpRequestPtr& request, const ::drogon::HttpResponsePtr& response) const noexcept; + + inline internal_drogon(const std::string& authorization): m_authorization(authorization) {} + + public: + internal_drogon() = delete; + }; + + /** + * @brief An abstract class that receives a Top.gg webhook event. Designed for use as a drogon::HttpSimpleController in drogon. + * + * @see topgg::webhook::vote + * @since 3.0.0 + */ + template + class drogon: private internal_drogon { + public: + drogon() = delete; + + inline drogon(const std::string& authorization): internal_drogon(authorization) { + static_assert(std::is_constructible_v, "T must be a valid model class."); + } + + void __handle(const ::drogon::HttpRequestPtr& request, std::function&& response) { + const auto response_data{::drogon::HttpResponse::newHttpResponse()}; + const auto json_data{parse(request, response_data)}; + + if (json_data.has_value()) { + const T data{json_data.value()}; + + callback(data); + } + + response(response_data); + } + + /** + * @brief The virtual callback that will be called whenever an incoming webhook request to the endpoint has been authenticated. + * + * @param T data The webhook event data. + * @since 3.0.0 + */ + virtual void callback(const T& data) = 0; + }; + }; // namespace webhook +}; // namespace topgg + +#undef __TOPGG_DROGON_WEBHOOKS__ \ No newline at end of file diff --git a/include/topgg/webhooks/models.h b/include/topgg/webhooks/models.h new file mode 100644 index 0000000..a4e61d9 --- /dev/null +++ b/include/topgg/webhooks/models.h @@ -0,0 +1,79 @@ +/** + * @module topgg + * @file models.h + * @brief A simple API wrapper for Top.gg written in C++. + * @authors Top.gg, null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-07-02 + * @version 3.0.0 + */ + +#pragma once + +#include + +#ifdef __TOPGG_DROGON_WEBHOOKS__ +#include +#else +#include +#endif + +#include +#include + +namespace topgg { + namespace webhook { +#ifdef __TOPGG_DROGON_WEBHOOKS__ + using json = Json::Value; +#else + using json = nlohmann::json; +#endif + + /** + * @brief A dispatched Top.gg vote webhook event. + * + * @since 3.0.0 + */ + class vote { + public: + TOPGG_EXPORT vote(const json& j); + + vote() = delete; + + /** + * @brief The ID of the Discord bot/server that received a vote. + * + * @since 3.0.0 + */ + std::string receiver_id; + + /** + * @brief The ID of the Top.gg user who voted. + * + * @since 3.0.0 + */ + std::string voter_id; + + /** + * @brief Whether this vote is just a test done from the page settings. + * + * @since 3.0.0 + */ + bool is_test; + + /** + * @brief Whether the weekend multiplier is active, where a single vote counts as two. + * + * @since 3.0.0 + */ + bool is_weekend; + + /** + * @brief Query strings found on the vote page. + * + * @since 3.0.0 + */ + std::unordered_map query; + }; + }; // namespace webhook +}; // namespace topgg \ No newline at end of file diff --git a/src/client.cpp b/src/client.cpp index a94ee2d..a32b91e 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -2,14 +2,101 @@ using topgg::client; -client::client(dpp::cluster& cluster, const std::string& token): m_token(token), m_cluster(cluster), m_autoposter_timer(0) { - m_headers.insert(std::pair("Authorization", "Bearer " + token)); - m_headers.insert(std::pair("Connection", "close")); +// 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 std::string id_from_bot_token(std::string bot_token) { + const auto pos{bot_token.find('.')}; + + if (pos != std::string::npos) { + std::string decoded_base64{}; + auto base64_input{bot_token.substr(0, pos)}; + const auto additional_equals{4 - (base64_input.length() % 4)}; + + for (size_t j{}; j < additional_equals; j++) { + base64_input.push_back('='); + } + + if (base64_decode(base64_input, decoded_base64)) { + return decoded_base64; + } + } + + throw std::invalid_argument{"Got a malformed API token."}; +} + +client::client(dpp::cluster& cluster, const std::string& token) + : m_token(token), m_cluster(cluster), m_autoposter_timer(0) { + m_id = id_from_bot_token(cluster.token); + + m_headers.insert(std::pair("Authorization", token)); 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) { +void client::get_bot(const dpp::snowflake bot_id, const topgg::get_bot_completion_event& callback) { basic_request("/bots/" + std::to_string(bot_id), callback, [](const auto& j) { return topgg::bot{j}; }); @@ -17,62 +104,76 @@ void client::get_bot(const dpp::snowflake bot_id, const topgg::get_bot_completio #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) { - return topgg::user{j}; - }); -} +size_t client::get_server_count() { +#ifdef __TOPGG_TESTING__ + return 2; +#else + size_t server_count{}; -#ifdef DPP_CORO -topgg::async_result client::co_get_user(const dpp::snowflake user_id) { - return topgg::async_result{ [this, user_id] (C&& cc) { return get_user(user_id, std::forward(cc)); }}; -} -#endif + for (auto& s: m_cluster.get_shards()) { + server_count += s.second->get_guild_count(); + } -void client::post_stats(const topgg::post_stats_completion_t& callback) { - post_stats(stats{m_cluster}, callback); + return server_count; +#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_server_count_inner(const size_t server_count, dpp::http_completion_event callback) { + auto headers{m_headers}; + dpp::json j{}; + + j["server_count"] = server_count; + + m_cluster.request(TOPGG_BASE_URL "/bots/stats", dpp::m_post, callback, j.dump(), "application/json", headers); } -#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_server_count(const topgg::post_server_count_completion_event& callback) { + const auto server_count{get_server_count()}; - 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_server_count_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_server_count() { + return dpp::async{[this](C&& cc) { return post_server_count(std::forward(cc)); }}; } #endif -void client::get_stats(const topgg::get_stats_completion_t& callback) { - basic_request("/bots/stats", callback, [](const auto& j) { - return topgg::stats{j}; +void client::get_server_count(const topgg::get_server_count_completion_event& callback) { + basic_request>("/bots/stats", callback, [](const auto& j) { + std::optional server_count{}; + + try { + *server_count = j["server_count"].template get(); + } catch (const std::exception&) {} + + return server_count; }); } #ifdef DPP_CORO -topgg::async_result client::co_get_stats() { - return topgg::async_result{ [this] (C&& cc) { return get_stats(std::forward(cc)); }}; +topgg::async_result> client::co_get_server_count() { + return topgg::async_result>{[this](C&& cc) { return get_server_count(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_event& callback) { + if (page < 1) { + page = 1; + } + + basic_request>("/bots/" + m_id + "/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,26 +183,29 @@ void client::get_voters(const topgg::get_voters_completion_t& callback) { }); } +void client::get_voters(const topgg::get_voters_completion_event& 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) { +void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_completion_event& callback) { + basic_request("/bots/check?userId=" + std::to_string(user_id), 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) { +void client::is_weekend(const topgg::is_weekend_completion_event& callback) { basic_request("/weekend", callback, [](const auto& j) { return j["is_weekend"].template get(); }); @@ -109,40 +213,70 @@ void client::is_weekend(const topgg::is_weekend_completion_t& callback) { #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::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::autopost_completion_event& 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) { + // clang-format off 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); + const auto server_count{get_server_count()}; + + if (server_count > 0) { + post_server_count_inner(server_count, [callback, server_count](const auto& response) { + if (response.error == dpp::h_success && response.status < 400) { + callback(std::optional{server_count}); + } else { + callback(std::nullopt); + } + }); + } + }, 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::autopost_completion_event& callback, time_t interval) { + if (!m_autoposter_timer) { + if (interval < TOPGG_AUTOPOSTER_MIN_INTERVAL) { + interval = TOPGG_AUTOPOSTER_MIN_INTERVAL; + } + + // clang-format off + m_autoposter_timer = m_cluster.start_timer([this, callback, source](TOPGG_UNUSED dpp::timer) { + const auto server_count{source->get_server_count(m_cluster)}; + + if (server_count > 0) { + post_server_count_inner(server_count, [callback, server_count](const auto& response) { + if (response.error == dpp::h_success && response.status < 400) { + callback(std::optional{server_count}); + } else { + callback(std::nullopt); + } + }); + } + }, interval, [source](TOPGG_UNUSED dpp::timer) { delete source; }); + // clang-format on + } +} + +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..b44d8d3 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -1,11 +1,5 @@ #include -using topgg::account; -using topgg::bot; -using topgg::stats; -using topgg::user; -using topgg::user_socials; - #ifdef _WIN32 #include #include @@ -21,92 +15,87 @@ static void strptime(const char* s, const char* f, tm* t) { #endif #endif -#define 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()}; +#define _TOPGG_SNOWFLAKE_FROM_JSON(j, name) \ + dpp::snowflake{j[#name].template get()} - DESERIALIZE(j, username, std::string); +using topgg::bot; +using topgg::bot_query; +using topgg::voter; - try { - const auto hash = j["avatar"].template get(); - const char* ext = hash.rfind("a_", 0) == 0 ? "gif" : "png"; +static time_t timestamp_from_id(const dpp::snowflake& id) { + return static_cast(((id >> 22) / 1000) + 1420070400); +} - avatar = "https://cdn.discordapp.com/avatars/" + std::to_string(id) + "/" + hash + "." + ext + "?size=1024"; - } catch (TOPGG_UNUSED const std::exception&) { - avatar = "https://cdn.discordapp.com/embed/avatars/" + std::to_string((id >> 22) % 5) + ".png"; - } +bot::bot(const dpp::json& j) { + id = _TOPGG_SNOWFLAKE_FROM_JSON(j, clientid); + topgg_id = _TOPGG_SNOWFLAKE_FROM_JSON(j, id); - created_at = static_cast(((id >> 22) / 1000) + 1420070400); -} + _TOPGG_DESERIALIZE(j, username, std::string); + _TOPGG_DESERIALIZE(j, avatar, std::string); -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); + created_at = timestamp_from_id(id); - IGNORE_EXCEPTION({ - const auto j_owners = j["owners"].template get>(); + _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,138 +104,108 @@ 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); - } +static std::string querystring(const std::string& value) { + static constexpr char hex[] = "0123456789abcdef"; + std::string output{}; - IGNORE_EXCEPTION({ - const auto j_support = j["support"].template get(); + output.reserve(value.length()); - if (j_support.size() > 0) { - support = std::optional{"https://discord.com/invite/" + j_support}; - } - }); + for (size_t i{}; i < value.length(); i++) { + const auto c{value[i]}; - try { - DESERIALIZE(j, shard_count, size_t); - } catch (TOPGG_UNUSED const std::exception&) { - shard_count = shards.size(); + 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]); + } } - try { - url.append(j["vanity"].template get()); - } catch (TOPGG_UNUSED const std::exception&) { - url.append(std::to_string(id)); - } + return output; } -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); +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))); } -stats::stats(dpp::cluster& bot) { - std::vector shards_server_count{}; - size_t servers{}; - - shards_server_count.reserve(bot.numshards); - - for (auto& s: bot.get_shards()) { - const auto server_count = s.second->get_guild_count(); - - servers += server_count; - shards_server_count.push_back(server_count); +void bot_query::send(const topgg::get_bots_completion_event& callback) { + std::string path{"/bots?"}; + + if (m_sort != nullptr) { + path.append("sort="); + path.append(m_sort); + path.push_back('&'); } - - m_server_count = std::optional{servers}; - m_shards = std::optional{shards_server_count}; - m_shard_id = std::optional{0}; - m_shard_count = std::optional{bot.numshards}; -} -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."}; + for (const auto& additional_query: m_query) { + path.append(additional_query.first); + path.push_back('='); + path.append(additional_query.second); + path.push_back('&'); } - m_shard_id = std::optional{shard_index}; - m_shard_count = std::optional{shards.size()}; -} + path.pop_back(); -std::string stats::to_json() const { - dpp::json j; + m_client->basic_request>(path, callback, [](const auto& j) { + std::vector bots{}; - SERIALIZE_PRIVATE_OPTIONAL(j, shard_count); - SERIALIZE_PRIVATE_OPTIONAL(j, server_count); - SERIALIZE_PRIVATE_OPTIONAL(j, shards); - SERIALIZE_PRIVATE_OPTIONAL(j, shard_id); + bots.reserve(j["count"].template get()); - return j.dump(); -} + for (const auto& bot: j["results"].template get>()) { + bots.push_back(topgg::bot{bot}); + } -std::vector stats::shards() const noexcept { - return m_shards.value_or(std::vector{}); + return bots; + }); } -size_t stats::shard_count() const noexcept { - return m_shard_count.value_or(shards().size()); +#ifdef DPP_CORO +dpp::async> bot_query::co_send() { + return dpp::async>{[this](C&& cc) { return send(std::forward(cc)); }}; } +#endif -std::optional stats::server_count() const noexcept { - if (m_server_count.has_value()) { - return m_server_count; - } else { - IGNORE_EXCEPTION({ - const auto& shards = m_shards.value(); +voter::voter(const dpp::json& j) { + id = _TOPGG_SNOWFLAKE_FROM_JSON(j, id); - if (shards.size() > 0) { - return std::optional{std::reduce(shards.begin(), shards.end())}; - } - }); + _TOPGG_DESERIALIZE(j, username, std::string); + _TOPGG_DESERIALIZE(j, avatar, std::string); - return std::nullopt; - } + created_at = timestamp_from_id(id); } -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); +std::string topgg::widget::large(const char* ty, const dpp::snowflake id) { + return TOPGG_BASE_URL "/widgets/large/" + std::string{ty} + "/" + std::to_string(id); } -user::user(const dpp::json& j) - : account(j) { - DESERIALIZE_OPTIONAL_STRING(j, bio); - DESERIALIZE_OPTIONAL_STRING(j, banner); - - if (j.contains("socials")) { - socials = std::optional{user_socials{j["socials"].template get()}}; - } +std::string topgg::widget::votes(const char* ty, const dpp::snowflake id) { + return TOPGG_BASE_URL "/widgets/small/votes/" + std::string{ty} + "/" + std::to_string(id); +} - 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); +std::string topgg::widget::owner(const char* ty, const dpp::snowflake id) { + return TOPGG_BASE_URL "/widgets/small/owner/" + std::string{ty} + "/" + std::to_string(id); } + +std::string topgg::widget::social(const char* ty, const dpp::snowflake id) { + return TOPGG_BASE_URL "/widgets/small/social/" + std::string{ty} + "/" + std::to_string(id); +} \ No newline at end of file diff --git a/src/result.cpp b/src/result.cpp index 3719440..97d085a 100644 --- a/src/result.cpp +++ b/src/result.cpp @@ -72,8 +72,8 @@ void internal_result::prepare() const { throw not_found{}; case 429: { - const auto j = json::parse(m_response.body); - const auto retry_after = j["retry_after"].template get(); + const auto j{json::parse(m_response.body)}; + const auto retry_after{j["retry_after"].template get()}; throw ratelimited{retry_after}; } diff --git a/src/webhooks/cpp-httplib.cpp b/src/webhooks/cpp-httplib.cpp new file mode 100644 index 0000000..d9be493 --- /dev/null +++ b/src/webhooks/cpp-httplib.cpp @@ -0,0 +1,35 @@ +#include + +#include + +using namespace topgg::webhook; + +std::optional internal_cpp_httplib::parse(const httplib::Request& request, httplib::Response& response) const noexcept { + if (request.method != "POST") { + response.status = 405; + response.set_content("Method not allowed", "text/plain"); + + return std::nullopt; + } + + const auto authorization{request.headers.find("Authorization")}; + + if (authorization == request.headers.end() || authorization->second != m_authorization) { + response.status = 401; + response.set_content("Unauthorized", "text/plain"); + + return std::nullopt; + } + + try { + const auto json_body{json::parse(request.body)}; + response.status = 204; + + return json_body; + } catch (TOPGG_UNUSED const std::exception&) { + response.status = 400; + response.set_content("Invalid JSON body", "text/plain"); + + return std::nullopt; + } +} \ No newline at end of file diff --git a/src/webhooks/drogon.cpp b/src/webhooks/drogon.cpp new file mode 100644 index 0000000..e6420a2 --- /dev/null +++ b/src/webhooks/drogon.cpp @@ -0,0 +1,45 @@ +#include + +#include + +using namespace topgg; + +std::optional webhook::internal_drogon::parse(const ::drogon::HttpRequestPtr& request, const ::drogon::HttpResponsePtr& response) const noexcept { + if (request->getMethod() != ::drogon::Post) { + response->setStatusCode(::drogon::k405MethodNotAllowed); + response->setContentTypeCode(::drogon::CT_TEXT_PLAIN); + response->setBody("Method not allowed"); + + return std::nullopt; + } + + const auto authorization{request->getHeader("Authorization")}; + + if (authorization != m_authorization) { + response->setStatusCode(::drogon::k401Unauthorized); + response->setContentTypeCode(::drogon::CT_TEXT_PLAIN); + response->setBody("Unauthorized"); + + return std::nullopt; + } + + const std::string json_body{request->body()}; + + Json::CharReaderBuilder builder{}; + const auto reader{builder.newCharReader()}; + + std::string errors{}; + Json::Value root{}; + + if (!reader->parse(json_body.c_str(), json_body.c_str() + json_body.size(), &root, &errors)) { + response->setStatusCode(::drogon::k400BadRequest); + response->setContentTypeCode(::drogon::CT_TEXT_PLAIN); + response->setBody("Invalid webhook::json body"); + + return std::nullopt; + } + + response->setStatusCode(::drogon::k204NoContent); + + return root; +} \ No newline at end of file diff --git a/src/webhooks/models.cpp b/src/webhooks/models.cpp new file mode 100644 index 0000000..06de76d --- /dev/null +++ b/src/webhooks/models.cpp @@ -0,0 +1,69 @@ +#include + +#include +#include + +using namespace topgg::webhook; + +static std::unordered_map parse_query_string(const std::string& query) { + std::unordered_map output{}; + + std::istringstream ss{query.substr(query.find('?') == 0 ? 1 : 0)}; + std::string pair{}; + + while (std::getline(ss, pair, '&')) { + const auto eq_pos{pair.find('=')}; + + if (eq_pos != std::string::npos) { + output[pair.substr(0, eq_pos)] = pair.substr(eq_pos + 1); + } + } + + return output; +} + +vote::vote(const json& j) { +#ifdef __TOPGG_DROGON_WEBHOOKS__ + receiver_id = j["bot"].asString(); + + if (receiver_id.empty()) { + receiver_id = j["guild"].asString(); + } + + voter_id = j["user"].asString(); + is_test = j["type"].asString() == "test"; + is_weekend = j.get("isWeekend", false).asBool(); + + const auto query_string{j["query"].asString()}; + + query = parse_query_string(query_string); +#else + try { + receiver_id = j["bot"].template get(); + } catch (TOPGG_UNUSED const std::exception&) { + receiver_id = j["guild"].template get(); + } + + voter_id = j["user"].template get(); + + try { + const auto type{j["type"].template get()}; + + is_test = type == "test"; + } catch (TOPGG_UNUSED const std::exception&) { + is_test = false; + } + + try { + is_weekend = j["isWeekend"].template get(); + } catch (TOPGG_UNUSED const std::exception&) { + is_weekend = false; + } + + try { + const auto query_string{j["query"].template get()}; + + query = parse_query_string(query_string); + } catch (TOPGG_UNUSED const std::exception&) {} +#endif +} \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..c89962c --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,78 @@ +cmake_minimum_required(VERSION 3.15) + +project( + topgg_tests + LANGUAGES CXX + HOMEPAGE_URL "https://docs.top.gg/docs" + DESCRIPTION "Unit tests for testing the Top.gg C++ SDK." +) + +set(TESTING ON) +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(TEST_API "Build unit tests for testing primary API support" ON) +option(TEST_AUTOPOSTER "Build unit tests for testing autoposter support" OFF) +option(TEST_CPP_HTTPLIB_WEBHOOKS "Build unit tests for testing webhooks via cpp-httplib" OFF) +option(TEST_DROGON_WEBHOOKS "Build unit tests for testing webhooks via drogon" OFF) + +if(NOT TEST_API AND NOT TEST_AUTOPOSTER) +set(ENABLE_API OFF) +endif() + +if(TEST_CPP_HTTPLIB_WEBHOOKS) +set(ENABLE_CPP_HTTPLIB_WEBHOOKS ON) + +if(TEST_DROGON_WEBHOOKS) +message(FATAL_ERROR "TEST_CPP_HTTPLIB_WEBHOOKS and TEST_DROGON_WEBHOOKS must NOT be ON at the same time.") +endif() +elseif(TEST_DROGON_WEBHOOKS) +set(ENABLE_DROGON_WEBHOOKS ON) +endif() + +list(APPEND CMAKE_MODULE_PATH "${CMAKE_CURRENT_SOURCE_DIR}/../cmake") +add_subdirectory(.. topgg) + +function(add_test_file FILENAME DIRECTORY) +add_executable(${FILENAME} ${DIRECTORY}/${FILENAME}.cpp) + +target_link_libraries(${FILENAME} PRIVATE topgg) + +set_target_properties(${FILENAME} PROPERTIES + CXX_STANDARD 20 + CXX_STANDARD_REQUIRED ON +) + +if(MSVC) +target_compile_options(${FILENAME} PUBLIC /nologo $<$:/diagnostics:caret /MDd /DDEBUG /D_DEBUG> $<$:/MD /O2 /Oi /Oy /Gy /DNDEBUG>) +else() +target_compile_options(${FILENAME} PUBLIC $<$:-O3> -Wall -Wextra -Wpedantic -Wformat=2 -Wnull-dereference -Wuninitialized -Wdeprecated) +endif() +endfunction() + +if(TEST_API) +add_test_file(test_api .) +endif() + +if(TEST_AUTOPOSTER) +add_test_file(test_autoposter .) +endif() + +if(ENABLE_CPP_HTTPLIB_WEBHOOKS) +file(GLOB WEBHOOKS cpp-httplib-webhooks/*.cpp) + +foreach(WEBHOOK ${WEBHOOKS}) +get_filename_component(NAME_WE ${WEBHOOK} NAME_WE) +add_test_file(${NAME_WE} cpp-httplib-webhooks) +endforeach() +elseif(ENABLE_DROGON_WEBHOOKS) +file(GLOB WEBHOOKS drogon-webhooks/*.cpp) + +foreach(WEBHOOK ${WEBHOOKS}) +get_filename_component(NAME_WE ${WEBHOOK} NAME_WE) +add_test_file(${NAME_WE} drogon-webhooks) +target_compile_definitions(${NAME_WE} PRIVATE __TOPGG_DROGON_WEBHOOKS__) +endforeach() +endif() \ No newline at end of file diff --git a/tests/cpp-httplib-webhooks/test_vote.cpp b/tests/cpp-httplib-webhooks/test_vote.cpp new file mode 100644 index 0000000..d52ffef --- /dev/null +++ b/tests/cpp-httplib-webhooks/test_vote.cpp @@ -0,0 +1,61 @@ +#include + +#include +#include +#include +#include + +template +using cpp_httplib_webhook = topgg::webhook::cpp_httplib; +using topgg::webhook::vote; + +class my_vote_listener: public cpp_httplib_webhook { +public: + inline my_vote_listener(const std::string& authorization): cpp_httplib_webhook(authorization) {} + + void callback(const vote& v) override { + std::cout << "A user with the ID of " << v.voter_id << " has voted us on Top.gg!" << std::endl; + } +}; + +int main() { + const auto authorization{std::getenv("MY_TOPGG_WEBHOOK_SECRET")}; + + if (authorization == nullptr) { + std::cerr << "error: missing MY_TOPGG_WEBHOOK_SECRET environment variable" << std::endl; + return 1; + } + + httplib::Server server{}; + my_vote_listener webhook{authorization}; + + server.Post("/votes", webhook.endpoint()); + + std::thread server_thread{[&server]() { + server.listen("localhost", 8080); + }}; + + std::this_thread::sleep_for(std::chrono::seconds{5}); + + httplib::Client client{"http://localhost:8080"}; + + const httplib::Headers headers = { + { "Authorization", authorization }, + { "Content-Type", "application/json" } + }; + + const auto json{R"({"bot":"12345","user":"12345","isWeekend":true,"type":"test"})"}; + + const auto response{client.Post("/votes", headers, json, "application/json")}; + + if (response && response->status == 204) { + std::cout << "ok" << std::endl; + } else { + std::cerr << "failed" << std::endl; + } + + server.stop(); + server_thread.join(); + + return 0; +} \ No newline at end of file diff --git a/tests/drogon-webhooks/test_vote.cpp b/tests/drogon-webhooks/test_vote.cpp new file mode 100644 index 0000000..600bf5c --- /dev/null +++ b/tests/drogon-webhooks/test_vote.cpp @@ -0,0 +1,77 @@ +#include + +#include +#include +#include +#include +#include + +template +using drogon_webhook = topgg::webhook::drogon; +using topgg::webhook::vote; + +class my_vote_listener: public ::drogon::HttpSimpleController, public drogon_webhook { +public: + inline my_vote_listener(const std::string& authorization): drogon_webhook(authorization) {} + + TOPGG_DROGON_WEBHOOK(); + + PATH_LIST_BEGIN + PATH_ADD("/votes", ::drogon::Post); + PATH_LIST_END + + void callback(const vote& v) override { + std::cout << "A user with the ID of " << v.voter_id << " has voted us on Top.gg!" << std::endl; + } +}; + +int main() { + const auto authorization{std::getenv("MY_TOPGG_WEBHOOK_SECRET")}; + + if (authorization == nullptr) { + std::cerr << "error: missing MY_TOPGG_WEBHOOK_SECRET environment variable" << std::endl; + return 1; + } + + auto& app{drogon::app()}; + + app.registerController(std::make_shared(authorization)); + + std::thread server_thread([&app]() { + app.addListener("127.0.0.1", 8080); + app.run(); + }); + + std::this_thread::sleep_for(std::chrono::seconds{5}); + + const auto client{drogon::HttpClient::newHttpClient("http://127.0.0.1:8080")}; + + Json::Value body{}; + + body["bot"] = "12345"; + body["user"] = "12345"; + body["isWeekend"] = true; + body["type"] = "test"; + + const auto request{drogon::HttpRequest::newHttpJsonRequest(body)}; + + request->setMethod(drogon::Post); + request->setPath("/votes"); + request->addHeader("Authorization", authorization); + + client->sendRequest(request, [&app](drogon::ReqResult result, const drogon::HttpResponsePtr& response) { + const auto status_code{response->statusCode()}; + + if (result == drogon::ReqResult::Ok && status_code == drogon::k204NoContent) { + std::cout << "ok" << std::endl; + } else { + std::cerr << "failed (" << static_cast(status_code) << ")" << std::endl; + } + + app.quit(); + }); + + server_thread.join(); + + return 0; +} \ No newline at end of file diff --git a/tests/test_api.cpp b/tests/test_api.cpp new file mode 100644 index 0000000..fbcfc9a --- /dev/null +++ b/tests/test_api.cpp @@ -0,0 +1,104 @@ +#include +#include + +#include +#include +#include +#include + +#define ACQUIRE_TEST_THREAD() \ + g_sem.acquire(); \ + if (g_exit_code != 0) { \ + goto TEST_END; \ + } \ + std::this_thread::sleep_for(1s) + +#define TEST_RESULT_CALLBACK() \ + [](const auto& raw_result) { \ + try { \ + const auto _result{raw_result.get()}; \ + std::cout << "ok" << std::endl; \ + } catch (const std::exception& exc) { \ + g_exit_code = 1; \ + std::cerr << "error: " << exc.what() << std::endl; \ + } \ + g_sem.release(); \ + } + +using namespace std::chrono_literals; + +static std::binary_semaphore g_sem{0}; +static int g_exit_code{}; + +int main() { + const auto discord_token{std::getenv("BOT_TOKEN")}; + const auto topgg_token{std::getenv("TOPGG_TOKEN")}; + + if (discord_token == nullptr) { + std::cerr << "error: missing BOT_TOKEN environment variable" << std::endl; + return 1; + } else if (topgg_token == nullptr) { + std::cerr << "error: missing TOPGG_TOKEN environment variable" << std::endl; + return 1; + } + + dpp::cluster bot{discord_token}; + topgg::client client{bot, topgg_token}; + + std::cout << "Starting bot... "; + + bot.start(dpp::start_type::st_return); + + std::cout << "ok\nget_bot "; + + client.get_bot(264811613708746752, TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "get_bots "; + + client + .get_bots() + .limit(250) + .skip(50) + .sort_by_monthly_votes() + .send(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "has_voted "; + + client.has_voted(661200758510977084, TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "post_server_count "; + + client.post_server_count([](const auto success) { + if (success) { + std::cout << "ok" << std::endl; + } else { + g_exit_code = 1; + std::cerr << "error" << std::endl; + } + + g_sem.release(); + }); + + ACQUIRE_TEST_THREAD(); + std::cout << "get_server_count "; + + client.get_server_count(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "get_voters "; + + client.get_voters(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "is_weekend "; + + client.is_weekend(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + +TEST_END: + return g_exit_code; +} diff --git a/tests/test_autoposter.cpp b/tests/test_autoposter.cpp new file mode 100644 index 0000000..0f96154 --- /dev/null +++ b/tests/test_autoposter.cpp @@ -0,0 +1,51 @@ +#include +#include + +#include +#include + +static std::binary_semaphore g_sem{0}; +static int g_exit_code{}; +static size_t g_counter{}; + +int main() { + const auto discord_token{std::getenv("BOT_TOKEN")}; + const auto topgg_token{std::getenv("TOPGG_TOKEN")}; + + if (discord_token == nullptr) { + std::cerr << "error: missing BOT_TOKEN environment variable" << std::endl; + return 1; + } else if (topgg_token == nullptr) { + std::cerr << "error: missing TOPGG_TOKEN environment variable" << std::endl; + return 1; + } + + dpp::cluster bot{discord_token}; + topgg::client client{bot, topgg_token}; + + std::cout << "Starting bot... "; + + bot.start(dpp::start_type::st_return); + + std::cout << "ok\n"; + + client.start_autoposter([](const auto& result) { + if (result) { + std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; + + if (g_counter++ == 3) { + g_sem.release(); + } + } else { + std::cerr << "Failed." << std::endl; + + g_exit_code = 1; + g_sem.release(); + } + }); + + g_sem.acquire(); + std::cout << "Done." << std::endl; + + return g_exit_code; +} diff --git a/topgg.rc b/topgg.rc index fc4d2cd..eafeae7 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 simple API wrapper for Top.gg written in C++." + VALUE "FileVersion", "3.0.0" + VALUE "ProductVersion", "3.0.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