From c1a70ba272c5baf765821f67546c0c5370fb5d1e Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 18 Feb 2025 00:49:59 +0700 Subject: [PATCH 01/41] feat: deprecate features no longer available in Top.gg API v0 --- include/topgg/models.h | 45 +++++----------------------- src/models.cpp | 66 +++++++++++------------------------------- 2 files changed, 24 insertions(+), 87 deletions(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index 672c632..9482e76 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -106,11 +106,7 @@ namespace topgg { public: bot() = delete; - /** - * @brief The Discord bot's discriminator. - * - * @since 2.0.0 - */ + [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be '0'.")]] std::string discriminator; /** @@ -163,11 +159,7 @@ namespace topgg { */ std::vector owners; - /** - * @brief A list of IDs of the guilds featured on this Discord bot’s page. - * - * @since 2.0.0 - */ + [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be an empty vector.")]] std::vector guilds; /** @@ -184,18 +176,10 @@ namespace topgg { */ time_t approved_at; - /** - * @brief Whether this Discord bot is Top.gg certified or not. - * - * @since 2.0.0 - */ + [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be false.")]] bool is_certified; - /** - * @brief A list of this Discord bot’s shards. - * - * @since 2.0.0 - */ + [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be an empty vector.")]] std::vector shards; /** @@ -219,11 +203,7 @@ namespace topgg { */ std::optional support; - /** - * @brief The amount of shards this Discord bot has according to posted stats. - * - * @since 2.0.0 - */ + [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be 0.")]] size_t shard_count; /** @@ -243,21 +223,10 @@ namespace topgg { 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 { + + class TOPGG_EXPORT [[deprecated("No longer has a use by Top.gg API v0. Soon, all you need is just your bot's server count (usize).")]] 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; diff --git a/src/models.cpp b/src/models.cpp index 7975681..24cb2d6 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -89,7 +89,7 @@ account::account(const dpp::json& j) { 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"; + avatar = "https://cdn.discordapp.com/embed/avatars/" + std::to_string((id >> 22) % 6) + ".png"; } created_at = static_cast(((id >> 22) / 1000) + 1420070400); @@ -97,7 +97,9 @@ account::account(const dpp::json& j) { bot::bot(const dpp::json& j) : account(j), url("https://top.gg/bot/") { - DESERIALIZE(j, discriminator, std::string); + // TODO: remove this soon + m_discriminator = "0"; + DESERIALIZE(j, prefix, std::string); DESERIALIZE_ALIAS(j, shortdesc, short_description, std::string); DESERIALIZE_OPTIONAL_STRING_ALIAS(j, longdesc, long_description); @@ -115,7 +117,6 @@ bot::bot(const dpp::json& j) } }); - DESERIALIZE_VECTOR(j, guilds, size_t); DESERIALIZE_OPTIONAL_STRING_ALIAS(j, bannerUrl, banner); const auto j_approved_at = j["date"].template get(); @@ -124,8 +125,9 @@ bot::bot(const dpp::json& j) strptime(j_approved_at.data(), "%Y-%m-%dT%H:%M:%S", &approved_at_tm); approved_at = mktime(&approved_at_tm); - DESERIALIZE_ALIAS(j, certifiedBot, is_certified, bool); - DESERIALIZE_VECTOR(j, shards, size_t); + // TODO: remove this soon + m_is_certified = false; + DESERIALIZE_ALIAS(j, points, votes, size_t); DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); @@ -143,11 +145,8 @@ bot::bot(const dpp::json& j) } }); - try { - DESERIALIZE(j, shard_count, size_t); - } catch (TOPGG_UNUSED const std::exception&) { - shard_count = shards.size(); - } + // TODO: remove this soon + shard_count = 0; try { url.append(j["vanity"].template get()); @@ -157,74 +156,43 @@ bot::bot(const dpp::json& j) } stats::stats(const dpp::json& j) { - DESERIALIZE_PRIVATE_OPTIONAL(j, shard_count, size_t); DESERIALIZE_PRIVATE_OPTIONAL(j, server_count, size_t); - DESERIALIZE_PRIVATE_OPTIONAL(j, shards, std::vector); - DESERIALIZE_PRIVATE_OPTIONAL(j, shard_id, size_t); } stats::stats(dpp::cluster& bot) { - 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); + servers += s.second->get_guild_count(); } 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."}; - } - - m_shard_id = std::optional{shard_index}; - m_shard_count = std::optional{shards.size()}; +// TODO: remove this soon +stats::stats(const std::vector& shards, const TOPGG_UNUSED size_t shard_index) + : m_shards(std::nullopt), m_server_count(std::optional{std::reduce(shards.begin(), shards.end())}) { + m_shard_id = 0; } std::string stats::to_json() const { dpp::json j; - SERIALIZE_PRIVATE_OPTIONAL(j, shard_count); SERIALIZE_PRIVATE_OPTIONAL(j, server_count); - SERIALIZE_PRIVATE_OPTIONAL(j, shards); - SERIALIZE_PRIVATE_OPTIONAL(j, shard_id); return j.dump(); } std::vector stats::shards() const noexcept { - return m_shards.value_or(std::vector{}); + return std::vector{}; } size_t stats::shard_count() const noexcept { - return m_shard_count.value_or(shards().size()); + return 0; } 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(); - - if (shards.size() > 0) { - return std::optional{std::reduce(shards.begin(), shards.end())}; - } - }); - - return std::nullopt; - } + return m_server_count; } user_socials::user_socials(const dpp::json& j) { From f8019c92f30195eacfb3657826ab6ed46755d5f3 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 18 Feb 2025 12:53:18 +0700 Subject: [PATCH 02/41] doc: remove usize --- include/topgg/models.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index 9482e76..42474c7 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -224,7 +224,7 @@ namespace topgg { }; - class TOPGG_EXPORT [[deprecated("No longer has a use by Top.gg API v0. Soon, all you need is just your bot's server count (usize).")]] stats { + class TOPGG_EXPORT [[deprecated("No longer has a use by Top.gg API v0. Soon, all you need is just your bot's server count.")]] stats { stats(const dpp::json& j); std::optional m_server_count; From dbc6d08c1c9975d1467afb59a612c163798a0153 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 01:46:26 +0700 Subject: [PATCH 03/41] feat: add bot_query --- include/topgg/client.h | 6 +++ include/topgg/models.h | 66 ++++++++++++++++++++++++++++---- src/models.cpp | 86 ++++++++++++++++++++++++++++++++++++++---- 3 files changed, 144 insertions(+), 14 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 3362394..ba7ce33 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -209,6 +209,10 @@ namespace topgg { topgg::async_result co_get_bot(const dpp::snowflake bot_id); #endif + inline bot_query get_bots() noexcept { + return bot_query{this}; + } + /** * @brief Fetches a user from a Discord ID. * @@ -721,5 +725,7 @@ namespace topgg { * @brief The destructor. Stops the autoposter if it's running. */ ~client(); + + friend class bot_query; }; }; // namespace topgg diff --git a/include/topgg/models.h b/include/topgg/models.h index 42474c7..dcbcb14 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -27,6 +27,24 @@ #undef _XOPEN_SOURCE #endif +#define TOPGG_BOT_QUERY_SORT(lib_name, api_name) \ + inline bot_query& sort_by_##lib_name() noexcept { \ + m_sort = #api_name; \ + return *this; \ + } + +#define TOPGG_BOT_QUERY_QUERY(type, name, ...) \ + inline bot_query& name(const type name) { \ + add_query(#name, name, __VA_ARGS__); \ + return *this; \ + } + +#define TOPGG_BOT_QUERY_SEARCH(type, name, ...) \ + inline bot_query& name(const type name) { \ + add_search(#name, name, __VA_ARGS__); \ + return *this; \ + } + namespace topgg { /** * @brief Base class of the account data stored in the Top.gg API. @@ -73,6 +91,7 @@ namespace topgg { time_t created_at; }; + class bot_query; class client; /** @@ -220,10 +239,43 @@ namespace topgg { */ std::string url; + friend class bot_query; friend class client; }; + using get_bots_completion_t = std::function>&)>; + + class TOPGG_EXPORT bot_query { + client* m_client; + std::string m_query; + std::string m_search; + const char* m_sort; + inline bot_query(client* c): m_client(c), m_query("/bots?"), m_sort(nullptr) {} + + void add_query(const char* key, const uint16_t value, const uint16_t max); + void add_query(const char* key, const char* value); + void add_search(const char* key, const std::string& value); + void add_search(const char* key, const size_t value); + + public: + bot_query() = delete; + + TOPGG_BOT_QUERY_SORT(approval_date, date); + TOPGG_BOT_QUERY_SORT(monthly_votes, monthlyPoints); + TOPGG_BOT_QUERY_QUERY(uint16_t, limit, 500); + TOPGG_BOT_QUERY_QUERY(uint16_t, skip, 499); + TOPGG_BOT_QUERY_SEARCH(std::string&, username); + TOPGG_BOT_QUERY_SEARCH(std::string&, prefix); + TOPGG_BOT_QUERY_SEARCH(size_t, votes); + TOPGG_BOT_QUERY_SEARCH(size_t, monthly_votes); + TOPGG_BOT_QUERY_SEARCH(std::string&, vanity); + + void finish(const get_bots_completion_t& callback); + + friend class client; + }; + class TOPGG_EXPORT [[deprecated("No longer has a use by Top.gg API v0. Soon, all you need is just your bot's server count.")]] stats { stats(const dpp::json& j); @@ -250,7 +302,7 @@ namespace topgg { * @since 2.0.0 */ inline stats(const size_t server_count, const size_t shard_count = 1) - : m_shard_count(std::optional{shard_count}), m_server_count(std::optional{server_count}) {} + : m_server_count(std::optional{server_count}) {} /** * @brief Creates a stats object based on the bot's shard data. @@ -391,11 +443,7 @@ namespace topgg { */ bool is_supporter; - /** - * @brief Whether this user is a Top.gg certified developer or not. - * - * @since 2.0.0 - */ + [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be false.")]] bool is_certified_dev; /** @@ -421,4 +469,8 @@ namespace topgg { friend class client; }; -}; // namespace topgg \ No newline at end of file +}; // 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/src/models.cpp b/src/models.cpp index 24cb2d6..ca00582 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -2,6 +2,7 @@ using topgg::account; using topgg::bot; +using topgg::bot_query; using topgg::stats; using topgg::user; using topgg::user_socials; @@ -78,6 +79,18 @@ static void strptime(const char* s, const char* f, tm* t) { } \ }) +#define ADD_QUERY(key, value) \ + m_query.append(key); \ + m_query.push_back('='); \ + m_query.append(value); \ + m_query.push_back('&') + +#define ADD_SEARCH(key, value) \ + m_query.append(key); \ + m_query.append("%3A%20"); \ + m_query.append(value); \ + m_query.append("%20") + account::account(const dpp::json& j) { id = dpp::snowflake{j["id"].template get()}; @@ -98,7 +111,7 @@ account::account(const dpp::json& j) { bot::bot(const dpp::json& j) : account(j), url("https://top.gg/bot/") { // TODO: remove this soon - m_discriminator = "0"; + discriminator = "0"; DESERIALIZE(j, prefix, std::string); DESERIALIZE_ALIAS(j, shortdesc, short_description, std::string); @@ -126,7 +139,7 @@ bot::bot(const dpp::json& j) approved_at = mktime(&approved_at_tm); // TODO: remove this soon - m_is_certified = false; + is_certified = false; DESERIALIZE_ALIAS(j, points, votes, size_t); DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); @@ -155,6 +168,65 @@ bot::bot(const dpp::json& j) } } +static std::string querystring(const std::string& value) { + static constexpr char hex[] = "0123456789abcdef"; + std::string output{}; + + output.reserve(value.length()); + + for (size_t i = 0; i < value.length(); i++) { + const auto c = value[i]; + + if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) { + output.push_back(c); + } else { + output.push_back('%'); + output.push_back(hex[(c >> 4) & 0x0f]); + output.push_back(hex[c & 0x0f]); + } + } + + return output; +} + +void bot_query::add_query(const char* key, const uint16_t value, const uint16_t max) { + ADD_QUERY(key, std::to_string(std::min(value, max))); +} + +void bot_query::add_query(const char* key, const char* value) { + ADD_QUERY(key, value); // querystring() not needed here +} + +void bot_query::add_search(const char* key, const std::string& value) { + ADD_SEARCH(key, querystring(value)); +} + +void bot_query::add_search(const char* key, const size_t value) { + ADD_SEARCH(key, std::to_string(value)); +} + +void bot_query::finish(const topgg::get_bots_completion_t& callback) { + if (m_sort != nullptr) { + add_query("sort", m_sort); + } + + if (!m_search.empty()) { + add_query("search", m_search.c_str()); + } + + m_query.pop_back(); + + m_client->basic_request>(m_query, callback, [](const auto& j) { + std::vector bots; + + for (const auto& part: j["results"].template get>()) { + bots.push_back(topgg::bot{part}); + } + + return bots; + }); +} + stats::stats(const dpp::json& j) { DESERIALIZE_PRIVATE_OPTIONAL(j, server_count, size_t); } @@ -171,9 +243,7 @@ stats::stats(dpp::cluster& bot) { // TODO: remove this soon stats::stats(const std::vector& shards, const TOPGG_UNUSED size_t shard_index) - : m_shards(std::nullopt), m_server_count(std::optional{std::reduce(shards.begin(), shards.end())}) { - m_shard_id = 0; -} + : m_server_count(std::optional{std::reduce(shards.begin(), shards.end())}) {} std::string stats::to_json() const { dpp::json j; @@ -184,7 +254,7 @@ std::string stats::to_json() const { } std::vector stats::shards() const noexcept { - return std::vector{}; + return std::vector{}; } size_t stats::shard_count() const noexcept { @@ -212,8 +282,10 @@ user::user(const dpp::json& j) socials = std::optional{user_socials{j["socials"].template get()}}; } + // TODO: remove this soon + is_certified_dev = false; + 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); From a80b1f95188c148f539ec8d907721575fc690b12 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 01:51:27 +0700 Subject: [PATCH 04/41] fix: woops --- src/models.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/models.cpp b/src/models.cpp index ca00582..a314d3e 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -86,10 +86,10 @@ static void strptime(const char* s, const char* f, tm* t) { m_query.push_back('&') #define ADD_SEARCH(key, value) \ - m_query.append(key); \ - m_query.append("%3A%20"); \ - m_query.append(value); \ - m_query.append("%20") + m_search.append(key); \ + m_search.append("%3A%20"); \ + m_search.append(value); \ + m_search.append("%20") account::account(const dpp::json& j) { id = dpp::snowflake{j["id"].template get()}; From f5073565fcfaa222c2d085899137cf72a40e4569 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 12:50:51 +0700 Subject: [PATCH 05/41] feat: add v0 --- README.md | 17 ++-- include/topgg/client.h | 190 ++++++++++++++--------------------------- include/topgg/models.h | 100 +++------------------- include/topgg/result.h | 4 +- include/topgg/topgg.h | 8 +- src/client.cpp | 83 +++++++++++------- src/models.cpp | 76 +++-------------- src/result.cpp | 4 +- 8 files changed, 157 insertions(+), 325 deletions(-) diff --git a/README.md b/README.md index 5a1debe..9eced8c 100644 --- a/README.md +++ b/README.md @@ -106,21 +106,21 @@ try { } ``` -### Posting your bot's statistics +### Posting your bot's server count ```cpp dpp::cluster bot{"your bot token"}; topgg::client topgg_client{bot, "your top.gg token"}; // using C++17 callbacks -topgg_client.post_stats([](const auto success) { +topgg_client.post_server_count([](const auto success) { if (success) { std::cout << "stats posted!" << std::endl; } }); // using C++20 coroutines -const auto success = co_await topgg_client.co_post_stats(); +const auto success = co_await topgg_client.co_post_server_count(); if (success) { std::cout << "stats posted!" << std::endl; @@ -168,10 +168,15 @@ topgg_client.start_autoposter(); ### Customized autoposting ```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.start_autoposter([](dpp::cluster& bot_inner) { - return topgg::stats{...}; -}); +topgg_client.start_autoposter(reinterpret_cast(new my_autoposter_source)); ``` \ No newline at end of file diff --git a/include/topgg/client.h b/include/topgg/client.h index ba7ce33..2ee1e46 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,8 +4,8 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-07-12 - * @version 2.0.0 + * @date 2025-02-19 + * @version 3.0.0 */ #pragma once @@ -35,12 +35,12 @@ namespace topgg { using get_user_completion_t = std::function&)>; /** - * @brief The callback function to call when get_stats completes. + * @brief The callback function to call when get_server_count 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_t = std::function>&)>; /** * @brief The callback function to call when get_voters completes. @@ -67,20 +67,12 @@ namespace topgg { using is_weekend_completion_t = std::function&)>; /** - * @brief The callback function to call when post_stats completes. - * - * @see topgg::client::post_stats - * @since 2.0.0 - */ - using post_stats_completion_t = std::function; - - /** - * @brief The callback function that retrieves the bot's stats. + * @brief The callback function to call when post_server_count completes. * - * @see topgg::client::start_autoposter - * @since 2.0.0 + * @see topgg::client::post_server_count + * @since 3.0.0 */ - using custom_autopost_callback_t = std::function<::topgg::stats(dpp::cluster&)>; + using post_server_count_completion_t = std::function; /** * @brief Main client class that lets you make HTTP requests with the Top.gg API. @@ -95,7 +87,14 @@ namespace topgg { 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); + + inline void post_server_count_inner(dpp::http_completion_event callback) { + return post_server_count_inner(get_server_count(), callback); } public: @@ -279,7 +278,7 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot’s statistics. + * @brief Fetches your Discord bot’s posted server count. * * Example: * @@ -287,29 +286,29 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client topgg_client{bot, "your top.gg token"}; * - * topgg_client.get_stats([](const auto& result) { + * topgg_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; * } * }); * ``` * - * @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_t& 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: * @@ -318,9 +317,9 @@ namespace topgg { * topgg::client topgg_client{bot, "your top.gg token"}; * * try { - * const auto stats = co_await topgg_client.co_get_stats(); + * const auto server_count = co_await topgg_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; * } @@ -331,14 +330,14 @@ namespace topgg { * @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. + * @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 /** @@ -367,7 +366,6 @@ namespace topgg { * @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 @@ -404,7 +402,6 @@ namespace topgg { * @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 @@ -436,7 +433,6 @@ namespace topgg { * @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 @@ -473,7 +469,6 @@ namespace topgg { * @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 @@ -545,7 +540,7 @@ namespace topgg { #endif /** - * @brief Manually posts your Discord bot's statistics using data directly from your D++ cluster instance. + * @brief Manually posts your Discord bot's server count using data directly from your D++ cluster instance. * * Example: * @@ -553,25 +548,25 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client topgg_client{bot, "your top.gg token"}; * - * topgg_client.post_stats([](const auto success) { + * topgg_client.post_server_count([](const auto success) { * if (success) { * 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_t& 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 Manually posts your Discord bot's server count using data directly from your D++ cluster instance through a C++20 coroutine. * * Example: * @@ -579,7 +574,7 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client topgg_client{bot, "your top.gg token"}; * - * const auto success = co_await topgg_client.co_post_stats(); + * const auto success = co_await topgg_client.co_post_server_count(); * * if (success) { * std::cout << "stats posted!" << std::endl; @@ -587,74 +582,16 @@ namespace topgg { * ``` * * @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. - * - * 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.post_stats(topgg::stats{server_count}, [](const auto success) { - * if (success) { - * std::cout << "stats posted!" << std::endl; - * } - * }); - * ``` - * - * @param s Your Discord bot's statistics. - * @param callback The callback function to call when post_stats completes. - * @note For its C++20 coroutine counterpart, see co_post_stats. - * @see topgg::result - * @see topgg::stats - * @see topgg::client::start_autoposter - * @see topgg::client::co_post_stats - * @since 2.0.0 - */ - void post_stats(const stats& s, const post_stats_completion_t& callback); - -#ifdef DPP_CORO - /** - * @brief Manually posts your Discord bot's statistics through a C++20 coroutine. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * const size_t server_count = 12345; - * const auto success = co_await topgg_client.co_post_stats(topgg::stats{server_count}); - * - * if (success) { - * std::cout << "stats posted!" << std::endl; - * } - * ``` - * - * @param 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 - * @since 2.0.0 - */ - dpp::async co_post_stats(const stats& s); -#endif - - /** - * @brief Starts autoposting statistics using data directly from your D++ cluster instance. + * @brief Starts autoposting your bot's server count using data directly from your D++ cluster instance. * * Example: * @@ -668,36 +605,41 @@ namespace topgg { * @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. * @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 * @since 2.0.0 */ void start_autoposter(const time_t delay = 1800); /** - * @brief Starts autoposting statistics. + * @brief Starts autoposting your 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"}; - * - * bot.start_autoposter([](dpp::cluster& bot_inner) { - * return topgg::stats{...}; - * }); + * + * topgg_client.start_autoposter(reinterpret_cast(new my_autoposter_source)); * ``` * - * @param callback The callback function that returns the current stats. + * @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 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. * @note This function has no effect if the autoposter is already running. - * @see topgg::stats - * @see topgg::client::post_stats + * @see topgg::client::autoposter_source + * @see topgg::client::post_server_count * @see topgg::client::stop_autoposter - * @since 2.0.0 + * @since 3.0.0 */ - void start_autoposter(const custom_autopost_callback_t& callback, const time_t delay = 1800); + void start_autoposter(autoposter_source* source, const time_t delay = 1800); /** * @brief Prematurely stops the autoposter. Calling this function is usually unnecessary as this function is called later in the destructor. @@ -716,7 +658,7 @@ namespace topgg { * ``` * * @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; diff --git a/include/topgg/models.h b/include/topgg/models.h index dcbcb14..d5546c4 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,8 +4,8 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-07-12 - * @version 2.0.0 + * @date 2025-02-19 + * @version 3.0.0 */ #pragma once @@ -125,9 +125,6 @@ namespace topgg { public: bot() = delete; - [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be '0'.")]] - std::string discriminator; - /** * @brief The Discord bot's command prefix. * @@ -178,9 +175,6 @@ namespace topgg { */ std::vector owners; - [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be an empty vector.")]] - std::vector guilds; - /** * @brief The Discord bot's page banner URL, if available. * @@ -195,12 +189,6 @@ namespace topgg { */ time_t approved_at; - [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be false.")]] - bool is_certified; - - [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be an empty vector.")]] - std::vector shards; - /** * @brief The amount of upvotes this Discord bot has. * @@ -222,9 +210,6 @@ namespace topgg { */ std::optional support; - [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be 0.")]] - size_t shard_count; - /** * @brief The invite URL of this Discord bot. * @@ -243,6 +228,13 @@ namespace topgg { friend class client; }; + /** + * @brief The callback function to call when get_bots completes. + * + * @see topgg::client::get_bots + * @see topgg::bot_query + * @since 3.0.0 + */ using get_bots_completion_t = std::function>&)>; class TOPGG_EXPORT bot_query { @@ -276,76 +268,9 @@ namespace topgg { friend class client; }; - class TOPGG_EXPORT [[deprecated("No longer has a use by Top.gg API v0. Soon, all you need is just your bot's server count.")]] stats { - stats(const dpp::json& j); - - std::optional m_server_count; - - std::string to_json() const; - + class autoposter_source { 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 - */ - inline stats(const size_t server_count, const size_t shard_count = 1) - : m_server_count(std::optional{server_count}) {} - - /** - * @brief Creates a stats object based on the bot's shard data. - * - * @param shards An array of this bot's server count for each shard. - * @param shard_index The array index of the shard posting this data, defaults to zero. - * @throw std::out_of_range If the shard_index argument is out of bounds from the shards argument. - * @since 2.0.0 - */ - stats(const std::vector& shards, const size_t shard_index = 0); - - /** - * @brief Returns this stats object's server count for each shard. - * @return std::vector This stats object's server count for each shard. - * @since 2.0.0 - */ - std::vector shards() const noexcept; - - /** - * @brief Returns this stats object's shard count. - * @return size_t This stats object's shard count. - * @since 2.0.0 - */ - size_t shard_count() const noexcept; - - /** - * @brief Returns this stats object's server count, if available. - * @return std::optional This stats object's server count, if available. - * @since 2.0.0 - */ - std::optional server_count() const noexcept; - - /** - * @brief Sets this stats object's server count. - * - * @param new_server_count The new server count. - * @since 2.0.0 - */ - inline void set_server_count(const size_t new_server_count) noexcept { - m_server_count = std::optional{new_server_count}; - } - - friend class client; + virtual size_t get_server_count(dpp::cluster&) = 0; }; class user; @@ -443,9 +368,6 @@ namespace topgg { */ bool is_supporter; - [[deprecated("No longer supported by Top.gg API v0. At the moment, this will always be false.")]] - bool is_certified_dev; - /** * @brief Whether this user is a Top.gg moderator or not. * diff --git a/include/topgg/result.h b/include/topgg/result.h index f0e3764..d0a572a 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,8 +4,8 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-07-12 - * @version 2.0.0 + * @date 2025-02-19 + * @version 3.0.0 */ #pragma once diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index eb92c90..a599bb9 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -3,9 +3,9 @@ * @file topgg.h * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 - * @date 2024-09-22 - * @version 2.0.0 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 + * @date 2025-02-19 + * @version 3.0.0 */ #pragma once @@ -48,6 +48,8 @@ #pragma clang diagnostic pop #endif +#define TOPGG_BASE_URL "https://top.gg/api" + #include #include #include \ No newline at end of file diff --git a/src/client.cpp b/src/client.cpp index a94ee2d..889a006 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -33,46 +33,61 @@ topgg::async_result client::co_get_user(const dpp::snowflake user_i } #endif -void client::post_stats(const topgg::post_stats_completion_t& callback) { - post_stats(stats{m_cluster}, callback); -} +size_t client::get_server_count() { + size_t server_count{}; -#ifdef DPP_CORO -dpp::async client::co_post_stats() { - return dpp::async{ [this] (C&& cc) { return post_stats(stats{m_cluster}, std::forward(cc)); }}; + for (auto& s: m_cluster.get_shards()) { + server_count += s.second->get_guild_count(); + } + + return server_count; } -#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_inner(const size_t server_count, dpp::http_completion_event callback) { + std::multimap headers{m_headers}; + dpp::json j{}; + j["server_count"] = server_count; + + const auto s_json{j.dump()}; headers.insert(std::pair("Content-Length", std::to_string(s_json.size()))); - 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); + m_cluster.request(TOPGG_BASE_URL "/bots/stats", dpp::m_post, callback, s_json, "application/json", headers); +} + +void client::post_server_count(const topgg::post_server_count_completion_t& callback) { + post_server_count_inner([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_t& 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; + std::vector voters{}; for (const auto& part: j) { voters.push_back(topgg::voter{part}); @@ -114,12 +129,6 @@ topgg::async_result client::co_is_weekend() { #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 */ @@ -131,18 +140,26 @@ void client::start_autoposter(const topgg::custom_autopost_callback_t& callback, * Create a D++ timer, this is managed by the D++ cluster and ticks every n seconds. * It can be stopped at any time without blocking, and does not need to create extra threads. */ - if (!m_autoposter_timer) { - m_autoposter_timer = m_cluster.start_timer([this, callback](TOPGG_UNUSED dpp::timer) { - const auto s = callback(m_cluster); - const auto s_json = s.to_json(); - std::multimap headers{m_headers}; - headers.insert(std::pair("Content-Length", std::to_string(s_json.length()))); - - m_cluster.request("https://top.gg/api/bots/stats", dpp::m_post, [](TOPGG_UNUSED const auto&) {}, s_json, "application/json", headers); + else if (!m_autoposter_timer) { + m_autoposter_timer = m_cluster.start_timer([this](TOPGG_UNUSED dpp::timer) { + post_server_count_inner([](TOPGG_UNUSED const auto&) {}); }, delay); } } +void client::start_autoposter(topgg::autoposter_source* source, const time_t delay) { + if (delay < 15 * 60) { + delete source; + throw std::invalid_argument{"Delay mustn't be shorter than 15 minutes."}; + } else if (!m_autoposter_timer) { + m_autoposter_timer = m_cluster.start_timer([this, source](TOPGG_UNUSED dpp::timer) { + post_server_count_inner(source->get_server_count(m_cluster), [](TOPGG_UNUSED const auto&) {}); + }, delay, [source](TOPGG_UNUSED dpp::timer) { + delete source; + }); + } +} + 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 a314d3e..dd98d26 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -3,7 +3,6 @@ using topgg::account; using topgg::bot; using topgg::bot_query; -using topgg::stats; using topgg::user; using topgg::user_socials; @@ -22,11 +21,6 @@ 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) \ name = j[#name].template get() @@ -63,7 +57,7 @@ static void strptime(const char* s, const char* f, tm* t) { #define DESERIALIZE_OPTIONAL_STRING(j, name) \ IGNORE_EXCEPTION({ \ - const auto value = j[#name].template get(); \ + const auto value{j[#name].template get()}; \ \ if (value.size() > 0) { \ name = std::optional{value}; \ @@ -72,7 +66,7 @@ static void strptime(const char* s, const char* f, tm* t) { #define DESERIALIZE_OPTIONAL_STRING_ALIAS(j, name, prop) \ IGNORE_EXCEPTION({ \ - const auto value = j[#name].template get(); \ + const auto value{j[#name].template get()}; \ \ if (value.size() > 0) { \ prop = std::optional{value}; \ @@ -97,8 +91,8 @@ account::account(const dpp::json& j) { DESERIALIZE(j, username, std::string); try { - const auto hash = j["avatar"].template get(); - const char* ext = hash.rfind("a_", 0) == 0 ? "gif" : "png"; + const auto hash{j["avatar"].template get()}; + const char* ext{hash.rfind("a_", 0) == 0 ? "gif" : "png"}; avatar = "https://cdn.discordapp.com/avatars/" + std::to_string(id) + "/" + hash + "." + ext + "?size=1024"; } catch (TOPGG_UNUSED const std::exception&) { @@ -110,9 +104,6 @@ account::account(const dpp::json& j) { bot::bot(const dpp::json& j) : account(j), url("https://top.gg/bot/") { - // TODO: remove this soon - discriminator = "0"; - DESERIALIZE(j, prefix, std::string); DESERIALIZE_ALIAS(j, shortdesc, short_description, std::string); DESERIALIZE_OPTIONAL_STRING_ALIAS(j, longdesc, long_description); @@ -121,7 +112,7 @@ bot::bot(const dpp::json& j) DESERIALIZE_OPTIONAL_STRING(j, github); IGNORE_EXCEPTION({ - const auto j_owners = j["owners"].template get>(); + const auto j_owners{j["owners"].template get>()}; owners.reserve(j_owners.size()); @@ -132,15 +123,12 @@ bot::bot(const dpp::json& j) DESERIALIZE_OPTIONAL_STRING_ALIAS(j, bannerUrl, banner); - const auto j_approved_at = j["date"].template get(); + const auto j_approved_at{j["date"].template get()}; tm approved_at_tm; strptime(j_approved_at.data(), "%Y-%m-%dT%H:%M:%S", &approved_at_tm); approved_at = mktime(&approved_at_tm); - // TODO: remove this soon - is_certified = false; - DESERIALIZE_ALIAS(j, points, votes, size_t); DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); @@ -151,16 +139,13 @@ bot::bot(const dpp::json& j) } IGNORE_EXCEPTION({ - const auto j_support = j["support"].template get(); + const auto j_support{j["support"].template get()}; if (j_support.size() > 0) { support = std::optional{"https://discord.com/invite/" + j_support}; } }); - // TODO: remove this soon - shard_count = 0; - try { url.append(j["vanity"].template get()); } catch (TOPGG_UNUSED const std::exception&) { @@ -174,8 +159,8 @@ static std::string querystring(const std::string& value) { output.reserve(value.length()); - for (size_t i = 0; i < value.length(); i++) { - const auto c = value[i]; + for (size_t i{}; i < value.length(); i++) { + const auto c{value[i]}; if (('a' <= c && c <= 'z') || ('A' <= c && c <= 'Z') || ('0' <= c && c <= '9')) { output.push_back(c); @@ -217,7 +202,7 @@ void bot_query::finish(const topgg::get_bots_completion_t& callback) { m_query.pop_back(); m_client->basic_request>(m_query, callback, [](const auto& j) { - std::vector bots; + std::vector bots{}; for (const auto& part: j["results"].template get>()) { bots.push_back(topgg::bot{part}); @@ -227,44 +212,6 @@ void bot_query::finish(const topgg::get_bots_completion_t& callback) { }); } -stats::stats(const dpp::json& j) { - DESERIALIZE_PRIVATE_OPTIONAL(j, server_count, size_t); -} - -stats::stats(dpp::cluster& bot) { - size_t servers{}; - - for (auto& s: bot.get_shards()) { - servers += s.second->get_guild_count(); - } - - m_server_count = std::optional{servers}; -} - -// TODO: remove this soon -stats::stats(const std::vector& shards, const TOPGG_UNUSED size_t shard_index) - : m_server_count(std::optional{std::reduce(shards.begin(), shards.end())}) {} - -std::string stats::to_json() const { - dpp::json j; - - SERIALIZE_PRIVATE_OPTIONAL(j, server_count); - - return j.dump(); -} - -std::vector stats::shards() const noexcept { - return std::vector{}; -} - -size_t stats::shard_count() const noexcept { - return 0; -} - -std::optional stats::server_count() const noexcept { - return m_server_count; -} - user_socials::user_socials(const dpp::json& j) { DESERIALIZE_OPTIONAL_STRING(j, github); DESERIALIZE_OPTIONAL_STRING(j, instagram); @@ -282,9 +229,6 @@ user::user(const dpp::json& j) socials = std::optional{user_socials{j["socials"].template get()}}; } - // TODO: remove this soon - is_certified_dev = false; - DESERIALIZE_ALIAS(j, supporter, is_supporter, bool); DESERIALIZE_ALIAS(j, mod, is_moderator, bool); DESERIALIZE_ALIAS(j, webMod, is_web_moderator, bool); 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}; } From c171af5f137cdb1443f15a778e3a027567d73674 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 13:52:19 +0700 Subject: [PATCH 06/41] fix: TOPGG_EXPORT this --- include/topgg/models.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index d5546c4..d2d373e 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -270,7 +270,7 @@ namespace topgg { class autoposter_source { public: - virtual size_t get_server_count(dpp::cluster&) = 0; + virtual size_t TOPGG_EXPORT get_server_count(dpp::cluster&) = 0; }; class user; From 28b8311428888ee5578e3f4ffc4a61a1757b4fa1 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 13:55:55 +0700 Subject: [PATCH 07/41] doc: add documentation for autoposter_source --- include/topgg/models.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/include/topgg/models.h b/include/topgg/models.h index d2d373e..5dc1e7f 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -268,6 +268,12 @@ namespace topgg { friend class client; }; + /** + * @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; From 56a82b68ece7da2775500a8f4ba3ccbe6a692bb2 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 14:06:31 +0700 Subject: [PATCH 08/41] meta: bump version and copyright year --- .gitattributes | 1 - LICENSE | 2 +- docs/Doxyfile | 5 ++-- docs/style.css | 58 ------------------------------------------ include/topgg/client.h | 2 +- include/topgg/models.h | 2 +- include/topgg/result.h | 2 +- topgg.rc | 6 ++--- 8 files changed, 9 insertions(+), 69 deletions(-) delete mode 100644 docs/style.css 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/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/docs/Doxyfile b/docs/Doxyfile index b18a078..7a947fd 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -48,7 +48,7 @@ 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 @@ -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/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 2ee1e46..88226c6 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -3,7 +3,7 @@ * @file client.h * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 * @date 2025-02-19 * @version 3.0.0 */ diff --git a/include/topgg/models.h b/include/topgg/models.h index 5dc1e7f..4ac51bd 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -3,7 +3,7 @@ * @file models.h * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 * @date 2025-02-19 * @version 3.0.0 */ diff --git a/include/topgg/result.h b/include/topgg/result.h index d0a572a..9dae587 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -3,7 +3,7 @@ * @file result.h * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 - * @copyright Copyright (c) 2024 Top.gg & null8626 + * @copyright Copyright (c) 2024-2025 Top.gg & null8626 * @date 2025-02-19 * @version 3.0.0 */ diff --git a/topgg.rc b/topgg.rc index fc4d2cd..97f97ba 100644 --- a/topgg.rc +++ b/topgg.rc @@ -17,11 +17,11 @@ BEGIN 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 "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 From 67738920d33307fff969a6bb74b05cdcf23bf2a6 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 15:42:45 +0700 Subject: [PATCH 09/41] doc: documentation tweaks --- README.md | 12 +-- include/topgg/client.h | 99 +++++++++++++++++----- include/topgg/models.h | 184 +++++++++++++++++++++++++++++++++++++++-- include/topgg/result.h | 8 +- 4 files changed, 263 insertions(+), 40 deletions(-) diff --git a/README.md b/README.md index 9eced8c..e48c23b 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ topgg_client.get_bot(264811613708746752, [](const auto& result) { 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; } }); @@ -75,7 +75,7 @@ try { 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; } ``` @@ -92,7 +92,7 @@ topgg_client.get_user(264811613708746752, [](const auto& result) { std::cout << user.username << std::endl; } catch (const std::exception& exc) { - std::cout << "error: " << exc.what() << std::endl; + std::cerr << "error: " << exc.what() << std::endl; } }); @@ -102,7 +102,7 @@ try { std::cout << user.username << std::endl; } catch (const std::exception& exc) { - std::cout << "error: " << exc.what() << std::endl; + std::cerr << "error: " << exc.what() << std::endl; } ``` @@ -140,7 +140,7 @@ topgg_client.has_voted(661200758510977084, [](const auto& result) { 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; } }); @@ -152,7 +152,7 @@ try { 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; } ``` diff --git a/include/topgg/client.h b/include/topgg/client.h index 88226c6..51d54d5 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -158,7 +158,7 @@ namespace topgg { * * 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; * } * }); * ``` @@ -188,7 +188,7 @@ namespace topgg { * * 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; * } * ``` * @@ -197,7 +197,7 @@ namespace topgg { * @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 dpp::http_error Thrown when 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 @@ -208,6 +208,61 @@ namespace topgg { topgg::async_result co_get_bot(const dpp::snowflake bot_id); #endif + /** + * @brief Queries/searches through the Top.gg database to look for matching listed Discord bots. + * + * C++17 example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client topgg_client{bot, "your top.gg token"}; + * + * topgg_client + * .get_bots() + * .limit(250) + * .skip(50) + * .username("shiro") + * .sort_by_monthly_votes() + * .finish([](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; + * } + * }); + * ``` + * + * C++20 example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client topgg_client{bot, "your top.gg token"}; + * + * try { + * const auto bots = co_await topgg_client + * .get_bots() + * .limit(250) + * .skip(50) + * .username("shiro") + * .sort_by_monthly_votes() + * .finish(); + * + * for (const auto& bot: bots) { + * std::cout << topgg_bot.username << std::endl; + * } + * } catch (const std::exception& exc) { + * std::cerr << "error: " << exc.what() << std::endl; + * } + * ``` + * + * @return bot_query An object for configuring the query in get_bots before being sent to the Top.gg API. + * @see topgg::bot_query + * @since 2.0.1 + */ inline bot_query get_bots() noexcept { return bot_query{this}; } @@ -227,7 +282,7 @@ namespace topgg { * * std::cout << user.username << std::endl; * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "error: " << exc.what() << std::endl; * } * }); * ``` @@ -257,7 +312,7 @@ namespace topgg { * * std::cout << user.username << std::endl; * } catch (const std::exception& exc) { - * std::cout << "error: " << exc.what() << std::endl; + * std::cerr << "error: " << exc.what() << std::endl; * } * ``` * @@ -266,7 +321,7 @@ namespace topgg { * @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 dpp::http_error Thrown when an unexpected HTTP exception has 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 @@ -278,7 +333,7 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot’s posted server count. + * @brief Fetches your Discord bot's posted server count. * * Example: * @@ -292,7 +347,7 @@ namespace topgg { * * 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; * } * }); * ``` @@ -308,7 +363,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot’s posted server count through a C++20 coroutine. + * @brief Fetches your Discord bot's posted server count through a C++20 coroutine. * * Example: * @@ -321,7 +376,7 @@ namespace topgg { * * 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; * } * ``` * @@ -329,7 +384,7 @@ namespace topgg { * @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 dpp::http_error Thrown when 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 @@ -341,7 +396,7 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot’s last 1000 voters. + * @brief Fetches your Discord bot's last 1000 voters. * * Example: * @@ -357,7 +412,7 @@ 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; * } * }); * ``` @@ -374,7 +429,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot’s last 1000 voters through a C++20 coroutine. + * @brief Fetches your Discord bot's last 1000 voters through a C++20 coroutine. * * Example: * @@ -389,7 +444,7 @@ 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; * } * ``` * @@ -397,7 +452,7 @@ namespace topgg { * @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 dpp::http_error Thrown when an unexpected HTTP exception has occured. * @return co_await to retrieve a std::vector if successful * @note For its C++17 callback-based counterpart, see get_voters. * @see topgg::async_result @@ -424,7 +479,7 @@ namespace topgg { * 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; * } * }); * ``` @@ -456,7 +511,7 @@ namespace topgg { * 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; * } * ``` * @@ -465,7 +520,7 @@ namespace topgg { * @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 dpp::http_error Thrown when 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 @@ -491,7 +546,7 @@ namespace topgg { * 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; * } * }); * ``` @@ -521,7 +576,7 @@ namespace topgg { * 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; * } * ``` * @@ -529,7 +584,7 @@ namespace topgg { * @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 dpp::http_error Thrown when 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 diff --git a/include/topgg/models.h b/include/topgg/models.h index 4ac51bd..192ea32 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -218,7 +218,7 @@ namespace topgg { std::string invite; /** - * @brief The URL of this Discord bot’s Top.gg page. + * @brief The URL of this Discord bot's Top.gg page. * * @since 2.0.0 */ @@ -233,10 +233,16 @@ namespace topgg { * * @see topgg::client::get_bots * @see topgg::bot_query - * @since 3.0.0 + * @since 2.0.1 */ using get_bots_completion_t = std::function>&)>; + /** + * @brief A class for configuring the query in get_bots before being sent to the Top.gg API. + * + * @see topgg::client::get_bots + * @since 2.0.1 + */ class TOPGG_EXPORT bot_query { client* m_client; std::string m_query; @@ -253,18 +259,180 @@ namespace topgg { public: bot_query() = delete; + /** + * @brief Sorts results based on each bot's ID. + * + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ + TOPGG_BOT_QUERY_SORT(id, id); + + /** + * @brief Sorts results based on each bot's approval date. + * + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_SORT(approval_date, date); + + /** + * @brief Sorts results based on each bot's monthly vote count. + * + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_SORT(monthly_votes, monthlyPoints); + + /** + * @brief Sets the maximum amount of bots to be queried. + * + * @param limit The maximum amount of bots to be queried. This cannot be more than 500. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_QUERY(uint16_t, limit, 500); + + /** + * @brief Sets the amount of bots to be skipped during the query. + * + * @param skip The amount of bots to be skipped during the query. This cannot be more than 499. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_QUERY(uint16_t, skip, 499); + + /** + * @brief Queries only Discord bots that has this username. + * + * @param username The bot's username. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_SEARCH(std::string&, username); + + /** + * @brief Queries only Discord bots that has this prefix. + * + * @param prefix The bot's prefix. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_SEARCH(std::string&, prefix); + + /** + * @brief Queries only Discord bots that has this vote count. + * + * @param votes The bot's vote count. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_SEARCH(size_t, votes); + + /** + * @brief Queries only Discord bots that has this monthly vote count. + * + * @param monthly_votes The bot's monthly vote count. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_SEARCH(size_t, monthly_votes); + + /** + * @brief Queries only Discord bots that has this vanity URL. + * + * @param vanity The bot's vanity URL. + * @return bot_query The current modified object. + * @see topgg::client::get_bots + * @since 2.0.1 + */ TOPGG_BOT_QUERY_SEARCH(std::string&, vanity); + /** + * @brief Sends the query to the Top.gg API. + * + * Example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client topgg_client{bot, "your top.gg token"}; + * + * topgg_client + * .get_bots() + * .limit(250) + * .skip(50) + * .username("shiro") + * .sort_by_monthly_votes() + * .finish([](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 callback The callback function to call when finish completes. + * @note For its C++20 coroutine counterpart, see co_finish. + * @see topgg::client::get_bots + * @see topgg::bot_query::co_finish + * @since 2.0.1 + */ void finish(const get_bots_completion_t& callback); +#ifdef DPP_CORO + /** + * @brief Sends the query to the Top.gg API through a C++20 coroutine. + * + * Example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client topgg_client{bot, "your top.gg token"}; + * + * try { + * const auto bots = co_await topgg_client + * .get_bots() + * .limit(250) + * .skip(50) + * .username("shiro") + * .sort_by_monthly_votes() + * .finish(); + * + * 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 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 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::co_finish + * @since 2.0.1 + */ + topgg::async_result> co_finish(); +#endif + friend class client; }; @@ -294,35 +462,35 @@ namespace topgg { user_socials() = delete; /** - * @brief A URL of this user’s GitHub account, if available. + * @brief A URL of this user's GitHub account, if available. * * @since 2.0.0 */ std::optional github; /** - * @brief A URL of this user’s Instagram account, if available. + * @brief A URL of this user's Instagram account, if available. * * @since 2.0.0 */ std::optional instagram; /** - * @brief A URL of this user’s Reddit account, if available. + * @brief A URL of this user's Reddit account, if available. * * @since 2.0.0 */ std::optional reddit; /** - * @brief A URL of this user’s Twitter/X account, if available. + * @brief A URL of this user's Twitter/X account, if available. * * @since 2.0.0 */ std::optional twitter; /** - * @brief A URL of this user’s YouTube channel, if available. + * @brief A URL of this user's YouTube channel, if available. * * @since 2.0.0 */ @@ -354,7 +522,7 @@ namespace topgg { std::optional bio; /** - * @brief The URL of this user’s profile banner image, if available. + * @brief The URL of this user's profile banner image, if available. * * @since 2.0.0 */ diff --git a/include/topgg/result.h b/include/topgg/result.h index 9dae587..cf238d1 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -123,7 +123,7 @@ namespace topgg { * @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 dpp::http_error Thrown when an unexpected HTTP exception has occured. * @return T The desired data, if successful. * @since 2.0.0 */ @@ -195,7 +195,7 @@ namespace topgg { * @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 dpp::http_error Thrown when an unexpected HTTP exception has occured. * @return T The desired data, if successful. * @see topgg::result::get * @since 2.0.0 @@ -211,7 +211,7 @@ namespace topgg { * @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 dpp::http_error Thrown when an unexpected HTTP exception has occured. * @return T The desired data, if successful. * @see topgg::result::get * @since 2.0.0 @@ -227,7 +227,7 @@ namespace topgg { * @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 dpp::http_error Thrown when an unexpected HTTP exception has occured. * @return T The desired data, if successful. * @see topgg::result::get * @since 2.0.0 From 71eeca7990f94b12a098df8f2d650cffd0aced22 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 19 Feb 2025 15:48:56 +0700 Subject: [PATCH 10/41] feat: add implementation for co_finish --- src/models.cpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/models.cpp b/src/models.cpp index dd98d26..9782c49 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -212,6 +212,12 @@ void bot_query::finish(const topgg::get_bots_completion_t& callback) { }); } +#ifdef DPP_CORO +dpp::async> bot_query::co_finish() { + return dpp::async>{ [this] (C&& cc) { return finish(std::forward(cc)); }}; +} +#endif + user_socials::user_socials(const dpp::json& j) { DESERIALIZE_OPTIONAL_STRING(j, github); DESERIALIZE_OPTIONAL_STRING(j, instagram); From f89509b2034e61edd4da2a436eca9f5c7b8c0e2e Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 20 Feb 2025 17:47:18 +0700 Subject: [PATCH 11/41] feat: add tests --- .github/workflows/test.yml | 29 +++++++++ README.md | 27 -------- include/topgg/client.h | 76 +---------------------- include/topgg/models.h | 122 +------------------------------------ include/topgg/result.h | 2 +- include/topgg/topgg.h | 2 +- src/client.cpp | 103 ++++++++++++++++++++++++++----- src/models.cpp | 27 +------- test.cpp | 66 ++++++++++++++++++++ 9 files changed, 189 insertions(+), 265 deletions(-) create mode 100644 .github/workflows/test.yml create mode 100644 test.cpp diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..466d9fa --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,29 @@ +name: Run tests +on: + push: + branches: [main, v0] + tags-ignore: ['**'] + paths: ['src/*.cpp', 'include/topgg/*.h'] + pull_request: + tags-ignore: ['**'] + paths: ['src/*.cpp', 'include/topgg/*.h'] +jobs: + test: + runs-on: windows-latest + steps: + - uses: actions/checkout@v4 + - uses: ilammy/msvc-dev-cmd@v1 + with: + arch: amd64 + - name: Build + run: | + cmake -B build . + cmake --build build --config Release + - name: Run tests + run: | + copy .\deps\*.dll + cl /nologo /EHsc /Iinclude /std:c++20 test.cpp .\deps\dpp.lib + .\test.exe + env: + BOT_TOKEN: ${{ secrets.BOT_TOKEN }} + TOPGG_TOKEN: ${{ secrets.TOPGG_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index e48c23b..f44ccd1 100644 --- a/README.md +++ b/README.md @@ -79,33 +79,6 @@ try { } ``` -### Fetching a user from its Discord ID - -```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; - -// using C++17 callbacks -topgg_client.get_user(264811613708746752, [](const auto& result) { - try { - const auto user = result.get(); - - std::cout << user.username << std::endl; - } catch (const std::exception& exc) { - std::cerr << "error: " << exc.what() << std::endl; - } -}); - -// using C++20 coroutines -try { - const auto user = co_await topgg_client.co_get_user(661200758510977084); - - std::cout << user.username << std::endl; -} catch (const std::exception& exc) { - std::cerr << "error: " << exc.what() << std::endl; -} -``` - ### Posting your bot's server count ```cpp diff --git a/include/topgg/client.h b/include/topgg/client.h index 51d54d5..a991c45 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-19 + * @date 2025-02-20 * @version 3.0.0 */ @@ -26,14 +26,6 @@ namespace topgg { */ using get_bot_completion_t = std::function&)>; - /** - * @brief The callback function to call when get_user 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_server_count completes. * @@ -82,6 +74,7 @@ namespace topgg { 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; @@ -267,71 +260,6 @@ namespace topgg { return bot_query{this}; } - /** - * @brief Fetches a user from a Discord ID. - * - * Example: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * topgg_client.get_user(661200758510977084, [](const auto& result) { - * try { - * const auto user = result.get(); - * - * std::cout << user.username << std::endl; - * } catch (const std::exception& exc) { - * std::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: - * - * ```cpp - * dpp::cluster bot{"your bot token"}; - * topgg::client topgg_client{bot, "your top.gg token"}; - * - * try { - * const auto user = co_await topgg_client.co_get_user(661200758510977084); - * - * std::cout << user.username << std::endl; - * } catch (const std::exception& exc) { - * std::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 has 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 - */ - topgg::async_result co_get_user(const dpp::snowflake user_id); -#endif - /** * @brief Fetches your Discord bot's posted server count. * diff --git a/include/topgg/models.h b/include/topgg/models.h index 192ea32..8b2da0d 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-19 + * @date 2025-02-20 * @version 3.0.0 */ @@ -50,7 +50,6 @@ namespace topgg { * @brief Base class of the account data stored in the Top.gg API. * * @see topgg::bot - * @see topgg::user * @see topgg::voter * @since 2.0.0 */ @@ -446,125 +445,6 @@ namespace topgg { public: virtual size_t TOPGG_EXPORT get_server_count(dpp::cluster&) = 0; }; - - class user; - - /** - * @brief Represents a user's social links, if available. - * - * @see topgg::user - * @since 2.0.0 - */ - class TOPGG_EXPORT user_socials { - user_socials(const dpp::json& j); - - public: - user_socials() = delete; - - /** - * @brief A URL of this user's GitHub account, if available. - * - * @since 2.0.0 - */ - std::optional github; - - /** - * @brief A URL of this user's Instagram account, if available. - * - * @since 2.0.0 - */ - std::optional instagram; - - /** - * @brief A URL of this user's Reddit account, if available. - * - * @since 2.0.0 - */ - std::optional reddit; - - /** - * @brief A URL of this user's Twitter/X account, if available. - * - * @since 2.0.0 - */ - std::optional twitter; - - /** - * @brief A URL of this user's YouTube channel, if available. - * - * @since 2.0.0 - */ - std::optional youtube; - - friend class user; - }; - - /** - * @brief Represents a user logged into Top.gg. - * - * @see topgg::user_socials - * @see topgg::client::get_user - * @see topgg::voter - * @see topgg::account - * @since 2.0.0 - */ - class TOPGG_EXPORT user: public account { - user(const dpp::json& j); - - public: - user() = delete; - - /** - * @brief The user's bio, if available. - * - * @since 2.0.0 - */ - std::optional bio; - - /** - * @brief The URL of this user's profile banner image, if available. - * - * @since 2.0.0 - */ - std::optional banner; - - /** - * @brief This user's social links, if available. - * - * @since 2.0.0 - */ - std::optional socials; - - /** - * @brief Whether this user is a Top.gg supporter or not. - * - * @since 2.0.0 - */ - bool is_supporter; - - /** - * @brief Whether this user is a Top.gg moderator or not. - * - * @since 2.0.0 - */ - bool is_moderator; - - /** - * @brief Whether this user is a Top.gg website moderator or not. - * - * @since 2.0.0 - */ - bool is_web_moderator; - - /** - * @brief Whether this user is a Top.gg website administrator or not. - * - * @since 2.0.0 - */ - bool is_admin; - - friend class client; - }; }; // namespace topgg #undef TOPGG_BOT_QUERY_SEARCH diff --git a/include/topgg/result.h b/include/topgg/result.h index cf238d1..59f1e90 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-19 + * @date 2025-02-20 * @version 3.0.0 */ diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index a599bb9..3ddd2f0 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-19 + * @date 2025-02-20 * @version 3.0.0 */ diff --git a/src/client.cpp b/src/client.cpp index 889a006..c1926eb 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -2,8 +2,93 @@ using topgg::client; +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 +}; + +static bool base64_decode(const std::string& input, std::string& output) { + size_t input_size = input.size(); + + if (input_size % 4 != 0) { + return false; + } + + size_t 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) { + size_t 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 = 0; 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 Discord Bot token."}; +} + 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_id = id_from_bot_token(cluster.token); + + m_headers.insert(std::pair("Authorization", token)); m_headers.insert(std::pair("Connection", "close")); m_headers.insert(std::pair("Content-Type", "application/json")); m_headers.insert(std::pair("User-Agent", "topgg (https://github.com/top-gg-community/cpp-sdk) D++")); @@ -21,18 +106,6 @@ topgg::async_result client::co_get_bot(const dpp::snowflake bot_id) } #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}; - }); -} - -#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 - size_t client::get_server_count() { size_t server_count{}; @@ -86,7 +159,7 @@ topgg::async_result> client::co_get_server_count() { #endif void client::get_voters(const topgg::get_voters_completion_t& callback) { - basic_request>("/bots/votes", callback, [](const auto& j) { + basic_request>("/bots/" + m_id + "/votes", callback, [](const auto& j) { std::vector voters{}; for (const auto& part: j) { @@ -105,7 +178,7 @@ topgg::async_result> client::co_get_voters() { void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_completion_t& callback) { - basic_request("/bots/votes?userId=" + std::to_string(user_id), callback, [](const auto& j) { + basic_request("/bots/" + m_id + "/votes?userId=" + std::to_string(user_id), callback, [](const auto& j) { return j["voted"].template get() != 0; }); } diff --git a/src/models.cpp b/src/models.cpp index 9782c49..a39110e 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -3,8 +3,6 @@ using topgg::account; using topgg::bot; using topgg::bot_query; -using topgg::user; -using topgg::user_socials; #ifdef _WIN32 #include @@ -216,27 +214,4 @@ void bot_query::finish(const topgg::get_bots_completion_t& callback) { dpp::async> bot_query::co_finish() { return dpp::async>{ [this] (C&& cc) { return finish(std::forward(cc)); }}; } -#endif - -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); -} - -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()}}; - } - - DESERIALIZE_ALIAS(j, supporter, is_supporter, bool); - DESERIALIZE_ALIAS(j, mod, is_moderator, bool); - DESERIALIZE_ALIAS(j, webMod, is_web_moderator, bool); - DESERIALIZE_ALIAS(j, admin, is_admin, bool); -} +#endif \ No newline at end of file diff --git a/test.cpp b/test.cpp new file mode 100644 index 0000000..75e86b0 --- /dev/null +++ b/test.cpp @@ -0,0 +1,66 @@ +#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) { \ + 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 = 0; + +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 topgg_client{bot, topgg_token}; + + std::cout << "get_bot "; + + topgg_client.get_bot(264811613708746752, TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "get_bots "; + + topgg_client + .get_bots() + .limit(250) + .skip(50) + .username("shiro") + .sort_by_monthly_votes() + .finish(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + +TEST_END: + return g_exit_code; +} \ No newline at end of file From a351b54e061c6e078d21d47415344847f02725fe Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 20 Feb 2025 17:57:22 +0700 Subject: [PATCH 12/41] fix: also include topgg.lib --- .github/workflows/test.yml | 3 ++- CMakeLists.txt | 2 +- include/topgg/client.h | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 466d9fa..c508ace 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -22,7 +22,8 @@ jobs: - name: Run tests run: | copy .\deps\*.dll - cl /nologo /EHsc /Iinclude /std:c++20 test.cpp .\deps\dpp.lib + copy .\build\Release\*.dll + cl /nologo /EHsc /Iinclude /std:c++20 test.cpp .\build\Release\topgg.lib .\deps\dpp.lib .\test.exe env: BOT_TOKEN: ${{ secrets.BOT_TOKEN }} diff --git a/CMakeLists.txt b/CMakeLists.txt index 5d97a2b..1c7b491 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -7,6 +7,7 @@ project( DESCRIPTION "The official C++ wrapper for the Top.gg API." ) +set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type") option(BUILD_SHARED_LIBS "Build shared libraries" ON) option(ENABLE_CORO "Support for C++20 coroutines" OFF) @@ -41,7 +42,6 @@ set_target_properties(topgg PROPERTIES ) set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) -set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) find_package(DPP REQUIRED) diff --git a/include/topgg/client.h b/include/topgg/client.h index a991c45..85b7c84 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -653,4 +653,4 @@ namespace topgg { friend class bot_query; }; -}; // namespace topgg +}; // namespace topgg \ No newline at end of file From 2400a8006c741875c932995a1e8457ce86486f44 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 25 Feb 2025 01:33:33 +0700 Subject: [PATCH 13/41] feat: almost everything works --- .gitignore | 5 +++- CMakeLists.txt | 14 +++++++--- include/topgg/client.h | 18 +++++-------- include/topgg/models.h | 52 ++++++++++++++++++++----------------- include/topgg/result.h | 6 ++--- include/topgg/topgg.h | 2 +- src/client.cpp | 58 ++++++++++++++++++++++-------------------- src/models.cpp | 19 +++----------- test.cpp | 44 +++++++++++++++++++++++++++++--- 9 files changed, 129 insertions(+), 89 deletions(-) diff --git a/.gitignore b/.gitignore index 968c13e..024ba54 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,7 @@ cmake-build-debug/ include/dpp/ build/ deps/ -docs/html/ \ No newline at end of file +docs/html/ + +test.exe +test.obj \ No newline at end of file diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c7b491..1a10848 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.8.2) +cmake_minimum_required(VERSION 3.10) project( topgg @@ -7,10 +7,12 @@ project( DESCRIPTION "The official C++ wrapper for the Top.gg API." ) +set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) -set(CMAKE_BUILD_TYPE Debug CACHE STRING "Build type") +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(TESTING "Enable this only if you are testing the library" OFF) file(GLOB TOPGG_SOURCE_FILES src/*.cpp) @@ -25,7 +27,7 @@ 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) @@ -35,6 +37,10 @@ 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} @@ -46,7 +52,7 @@ set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} ${CMAKE_SOURCE_DIR}/cmake) find_package(DPP REQUIRED) if(MSVC) -target_compile_options(topgg PUBLIC $<$:/diagnostics:caret /MTd> $<$:/MT /O2 /Oi /Oy /Gy>) +target_compile_options(topgg PUBLIC $<$:/diagnostics:caret /MTd /DDEBUG /D_DEBUG> $<$:/MT /O2 /Oi /Oy /Gy /DNDEBUG>) else() target_compile_options(topgg PUBLIC $<$:-O3> -Wall -Wextra -Wpedantic -Wformat=2 -Wnull-dereference -Wuninitialized -Wdeprecated) endif() diff --git a/include/topgg/client.h b/include/topgg/client.h index 85b7c84..362842c 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-20 + * @date 2025-02-25 * @version 3.0.0 */ @@ -85,10 +85,6 @@ namespace topgg { size_t get_server_count(); void post_server_count_inner(const size_t server_count, dpp::http_completion_event callback); - - inline void post_server_count_inner(dpp::http_completion_event callback) { - return post_server_count_inner(get_server_count(), callback); - } public: client() = delete; @@ -381,7 +377,7 @@ namespace topgg { * @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 has occured. - * @return co_await to retrieve a std::vector if successful + * @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 @@ -585,14 +581,13 @@ namespace topgg { * bot.start_autoposter(); * ``` * - * @param delay The minimum delay between post requests in seconds. Defaults to 30 minutes. - * @throw std::invalid_argument Throws if the delay argument is shorter than 15 minutes. + * @param delay The minimum delay between post requests 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 */ - void start_autoposter(const time_t delay = 1800); + void start_autoposter(time_t delay = 900); /** * @brief Starts autoposting your bot's server count using a custom data source. @@ -614,15 +609,14 @@ namespace topgg { * ``` * * @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 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 delay The minimum delay between post requests in seconds. Defaults to 15 minutes. * @note This function has no effect if the autoposter is already running. * @see topgg::client::autoposter_source * @see topgg::client::post_server_count * @see topgg::client::stop_autoposter * @since 3.0.0 */ - void start_autoposter(autoposter_source* source, const time_t delay = 1800); + void start_autoposter(autoposter_source* source, time_t delay = 900); /** * @brief Prematurely stops the autoposter. Calling this function is usually unnecessary as this function is called later in the destructor. diff --git a/include/topgg/models.h b/include/topgg/models.h index 8b2da0d..a4a8f10 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-20 + * @date 2025-02-25 * @version 3.0.0 */ @@ -61,29 +61,28 @@ namespace topgg { account() = delete; /** - * @brief The account's Discord ID. + * @brief This account's Discord ID. * * @since 2.0.0 */ dpp::snowflake id; /** - * @brief The account's entire Discord avatar URL. + * @brief This account's avatar URL. * - * @note This avatar URL can be animated if possible. * @since 2.0.0 */ std::string avatar; /** - * @brief The account's username. + * @brief This account's username. * - * @since 2.0.0 + * @since 3.0.0 */ - std::string username; + std::string name; /** - * @brief The unix timestamp of when this account was created. + * @brief When this account was created. * * @since 2.0.0 */ @@ -125,21 +124,21 @@ namespace topgg { bot() = delete; /** - * @brief The Discord bot's command prefix. + * @brief This bot's prefix. * * @since 2.0.0 */ std::string prefix; /** - * @brief The Discord bot's short description. + * @brief This bot's short description. * * @since 2.0.0 */ std::string short_description; /** - * @brief The Discord bot's long description, if available. + * @brief This bot's long description. * * @note This long description can contain Markdown and/or HTML. * @since 2.0.0 @@ -147,82 +146,89 @@ namespace topgg { std::optional long_description; /** - * @brief A list of the Discord bot's tags. + * @brief This bot's tags. * * @since 2.0.0 */ std::vector tags; /** - * @brief A link to the Discord bot's website, if available. + * @brief This bot's website URL. * * @since 2.0.0 */ std::optional website; /** - * @brief A link to the Discord bot's GitHub repository, if available. + * @brief This bot's GitHub repository URL. * * @since 2.0.0 */ std::optional github; /** - * @brief A list of the Discord bot's owners, represented in Discord user IDs. + * @brief This bot's owners IDs. * * @since 2.0.0 */ std::vector owners; /** - * @brief The Discord bot's page banner URL, if available. + * @brief This bot's banner URL. * * @since 2.0.0 */ std::optional banner; /** - * @brief The unix timestamp of when this Discord bot was approved on Top.gg by a Bot Reviewer. + * @brief When this bot was approved on Top.gg. * * @since 2.0.0 */ time_t approved_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 invite URL of this Discord bot. + * @brief This bot's invite URL. * * @since 2.0.0 */ - std::string invite; + std::optional invite; /** - * @brief The URL of this Discord bot's Top.gg page. + * @brief This bot's Top.gg page URL. * * @since 2.0.0 */ std::string url; + /** + * @brief This bot's posted server count. + * + * @since 3.0.0 + */ + std::optional server_count; + friend class bot_query; friend class client; }; diff --git a/include/topgg/result.h b/include/topgg/result.h index 59f1e90..5b456ae 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-20 + * @date 2025-02-25 * @version 3.0.0 */ @@ -81,7 +81,7 @@ namespace topgg { template class result; - class TOPGG_EXPORT internal_result { + class internal_result { const dpp::http_request_completion_t m_response; void prepare() const; @@ -106,7 +106,7 @@ namespace topgg { * @since 2.0.0 */ template - class TOPGG_EXPORT result { + class result { const internal_result m_internal; const std::function m_parse_fn; diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index 3ddd2f0..85bc83a 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,7 +4,7 @@ * @brief The official C++ wrapper for the Top.gg API. * @authors Top.gg, null8626 * @copyright Copyright (c) 2024-2025 Top.gg & null8626 - * @date 2025-02-20 + * @date 2025-02-25 * @version 3.0.0 */ diff --git a/src/client.cpp b/src/client.cpp index c1926eb..2c223d9 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -21,13 +21,13 @@ static constexpr unsigned char g_base64_decoding_table[] = { }; static bool base64_decode(const std::string& input, std::string& output) { - size_t input_size = input.size(); + const auto input_size{input.size()}; if (input_size % 4 != 0) { return false; } - size_t output_size = input_size / 4 * 3; + auto output_size{input_size / 4 * 3}; if (input_size >= 1 && input[input_size - 1] == '=') { output_size--; @@ -39,7 +39,7 @@ static bool base64_decode(const std::string& input, std::string& output) { output.resize(output_size); - uint32_t a, b, c, d, triple; + 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++])]; @@ -66,14 +66,14 @@ static bool base64_decode(const std::string& input, std::string& output) { } static std::string id_from_bot_token(std::string bot_token) { - size_t pos = bot_token.find('.'); + 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 = 0; j < additional_equals; j++) { + for (size_t j{}; j < additional_equals; j++) { base64_input.push_back('='); } @@ -89,7 +89,6 @@ client::client(dpp::cluster& cluster, const std::string& token): m_token(token), m_id = id_from_bot_token(cluster.token); m_headers.insert(std::pair("Authorization", token)); - m_headers.insert(std::pair("Connection", "close")); m_headers.insert(std::pair("Content-Type", "application/json")); m_headers.insert(std::pair("User-Agent", "topgg (https://github.com/top-gg-community/cpp-sdk) D++")); } @@ -117,19 +116,26 @@ size_t client::get_server_count() { } void client::post_server_count_inner(const size_t server_count, dpp::http_completion_event callback) { - std::multimap headers{m_headers}; + auto headers{m_headers}; dpp::json j{}; j["server_count"] = server_count; - const auto s_json{j.dump()}; - headers.insert(std::pair("Content-Length", std::to_string(s_json.size()))); - - m_cluster.request(TOPGG_BASE_URL "/bots/stats", dpp::m_post, callback, s_json, "application/json", headers); + m_cluster.request(TOPGG_BASE_URL "/bots/" + m_id + "/stats", dpp::m_post, callback, j.dump(), "application/json", headers); } -void client::post_server_count(const topgg::post_server_count_completion_t& callback) { - post_server_count_inner([callback](const auto& response) { +void client::post_server_count(const topgg::post_server_count_completion_t& callback) { + const auto server_count{get_server_count()}; + + if (server_count == 0) { +#ifdef __TOPGG_TESTING__ + return callback(true); +#else + return callback(false); +#endif + } + + post_server_count_inner(server_count, [callback](const auto& response) { callback(response.error == dpp::h_success && response.status < 400); }); } @@ -141,7 +147,7 @@ dpp::async client::co_post_server_count() { #endif void client::get_server_count(const topgg::get_server_count_completion_t& callback) { - basic_request>("/bots/stats", callback, [](const auto& j) { + basic_request>("/bots/" + m_id + "/stats", callback, [](const auto& j) { std::optional server_count{}; try { @@ -178,7 +184,7 @@ topgg::async_result> client::co_get_voters() { void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_completion_t& callback) { - basic_request("/bots/" + m_id + "/votes?userId=" + std::to_string(user_id), callback, [](const auto& j) { + basic_request("/bots/" + m_id + "/check?userId=" + std::to_string(user_id), callback, [](const auto& j) { return j["voted"].template get() != 0; }); } @@ -201,12 +207,9 @@ topgg::async_result client::co_is_weekend() { } #endif -void client::start_autoposter(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(time_t delay) { + if (delay < 900) { + delay = 900; } /** @@ -215,16 +218,17 @@ void client::start_autoposter(const time_t delay) { */ else if (!m_autoposter_timer) { m_autoposter_timer = m_cluster.start_timer([this](TOPGG_UNUSED dpp::timer) { - post_server_count_inner([](TOPGG_UNUSED const auto&) {}); + post_server_count_inner(get_server_count(), [](TOPGG_UNUSED const auto&) {}); }, delay); } } -void client::start_autoposter(topgg::autoposter_source* source, const time_t delay) { - if (delay < 15 * 60) { - delete source; - throw std::invalid_argument{"Delay mustn't be shorter than 15 minutes."}; - } else if (!m_autoposter_timer) { +void client::start_autoposter(topgg::autoposter_source* source, time_t delay) { + if (!m_autoposter_timer) { + if (delay < 900) { + delay = 900; + } + m_autoposter_timer = m_cluster.start_timer([this, source](TOPGG_UNUSED dpp::timer) { post_server_count_inner(source->get_server_count(m_cluster), [](TOPGG_UNUSED const auto&) {}); }, delay, [source](TOPGG_UNUSED dpp::timer) { diff --git a/src/models.cpp b/src/models.cpp index a39110e..ad698a2 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -86,7 +86,7 @@ static void strptime(const char* s, const char* f, tm* t) { account::account(const dpp::json& j) { id = dpp::snowflake{j["id"].template get()}; - DESERIALIZE(j, username, std::string); + DESERIALIZE_ALIAS(j, username, name, std::string); try { const auto hash{j["avatar"].template get()}; @@ -129,20 +129,9 @@ bot::bot(const dpp::json& j) DESERIALIZE_ALIAS(j, points, votes, size_t); DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, 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); - } - - IGNORE_EXCEPTION({ - const auto j_support{j["support"].template get()}; - - if (j_support.size() > 0) { - support = std::optional{"https://discord.com/invite/" + j_support}; - } - }); + DESERIALIZE_OPTIONAL(j, invite, std::string); + DESERIALIZE_OPTIONAL(j, support, std::string); + DESERIALIZE_OPTIONAL(j, server_count, size_t); try { url.append(j["vanity"].template get()); diff --git a/test.cpp b/test.cpp index 75e86b0..1b42925 100644 --- a/test.cpp +++ b/test.cpp @@ -19,6 +19,7 @@ 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(); \ @@ -27,7 +28,7 @@ using namespace std::chrono_literals; static std::binary_semaphore g_sem{0}; -static int g_exit_code = 0; +static int g_exit_code{}; int main() { const auto discord_token{std::getenv("BOT_TOKEN")}; @@ -44,10 +45,14 @@ int main() { dpp::cluster bot{discord_token}; topgg::client topgg_client{bot, topgg_token}; - std::cout << "get_bot "; + std::cout << "starting bot "; - topgg_client.get_bot(264811613708746752, TEST_RESULT_CALLBACK()); + bot.start(dpp::start_type::st_return); + + std::cout << "ok\nget_bot "; + topgg_client.get_bot(264811613708746752, TEST_RESULT_CALLBACK()); + ACQUIRE_TEST_THREAD(); std::cout << "get_bots "; @@ -60,6 +65,39 @@ int main() { .finish(TEST_RESULT_CALLBACK()); ACQUIRE_TEST_THREAD(); + std::cout << "has_voted "; + + topgg_client.has_voted(661200758510977084, TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "post_server_count "; + + topgg_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 "; + + topgg_client.get_server_count(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "get_voters "; + + topgg_client.get_voters(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); + std::cout << "is_weekend "; + + topgg_client.is_weekend(TEST_RESULT_CALLBACK()); + + ACQUIRE_TEST_THREAD(); TEST_END: return g_exit_code; From e8daa6d96db3909fd29cc8656f77c8f2e0b357b6 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 25 Feb 2025 01:35:25 +0700 Subject: [PATCH 14/41] feat: remove github workflows --- .github/workflows/test.yml | 30 ------------------------------ 1 file changed, 30 deletions(-) delete mode 100644 .github/workflows/test.yml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index c508ace..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Run tests -on: - push: - branches: [main, v0] - tags-ignore: ['**'] - paths: ['src/*.cpp', 'include/topgg/*.h'] - pull_request: - tags-ignore: ['**'] - paths: ['src/*.cpp', 'include/topgg/*.h'] -jobs: - test: - runs-on: windows-latest - steps: - - uses: actions/checkout@v4 - - uses: ilammy/msvc-dev-cmd@v1 - with: - arch: amd64 - - name: Build - run: | - cmake -B build . - cmake --build build --config Release - - name: Run tests - run: | - copy .\deps\*.dll - copy .\build\Release\*.dll - cl /nologo /EHsc /Iinclude /std:c++20 test.cpp .\build\Release\topgg.lib .\deps\dpp.lib - .\test.exe - env: - BOT_TOKEN: ${{ secrets.BOT_TOKEN }} - TOPGG_TOKEN: ${{ secrets.TOPGG_TOKEN }} \ No newline at end of file From 4ee2596e5ca765059673f1071ebf168551f818c5 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:38:47 +0700 Subject: [PATCH 15/41] style: add space --- test.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test.cpp b/test.cpp index 1b42925..0eade0f 100644 --- a/test.cpp +++ b/test.cpp @@ -82,6 +82,7 @@ int main() { g_sem.release(); }); + ACQUIRE_TEST_THREAD(); std::cout << "get_server_count "; @@ -101,4 +102,4 @@ int main() { TEST_END: return g_exit_code; -} \ No newline at end of file +} From fa1342294183528b49b70602262e8c9416b38cf2 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Tue, 25 Feb 2025 01:44:26 +0700 Subject: [PATCH 16/41] style: use braces --- test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test.cpp b/test.cpp index 0eade0f..ce7e18f 100644 --- a/test.cpp +++ b/test.cpp @@ -16,7 +16,7 @@ #define TEST_RESULT_CALLBACK() \ [](const auto& raw_result) { \ try { \ - const auto _result = raw_result.get(); \ + const auto _result{raw_result.get()}; \ std::cout << "ok" << std::endl; \ } catch (const std::exception& exc) { \ g_exit_code = 1; \ From 5f03e571343c0b5d8d53eb78e96194dadcd81c6a Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 25 Feb 2025 12:57:12 +0700 Subject: [PATCH 17/41] fix: adapt more stuff for v0 --- include/topgg/client.h | 12 ++++++------ include/topgg/models.h | 34 +++++++++++++++++----------------- src/models.cpp | 24 ++++++++---------------- test.cpp | 4 ++-- 4 files changed, 33 insertions(+), 41 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 362842c..43d4e2c 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -209,10 +209,10 @@ namespace topgg { * topgg_client * .get_bots() * .limit(250) - * .skip(50) + * .offset(50) * .username("shiro") * .sort_by_monthly_votes() - * .finish([](const auto& result) { + * .send([](const auto& result) { * try { * const auto bots = result.get(); * @@ -235,10 +235,10 @@ namespace topgg { * const auto bots = co_await topgg_client * .get_bots() * .limit(250) - * .skip(50) + * .offset(50) * .username("shiro") * .sort_by_monthly_votes() - * .finish(); + * .send(); * * for (const auto& bot: bots) { * std::cout << topgg_bot.username << std::endl; @@ -320,7 +320,7 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot's last 1000 voters. + * @brief Fetches your Discord bot's last 1000 unique voters. * * Example: * @@ -353,7 +353,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot's last 1000 voters through a C++20 coroutine. + * @brief Fetches your Discord bot's last 1000 unique voters through a C++20 coroutine. * * Example: * diff --git a/include/topgg/models.h b/include/topgg/models.h index a4a8f10..8bdba80 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -181,11 +181,11 @@ namespace topgg { std::optional banner; /** - * @brief When this bot was approved on Top.gg. + * @brief When this bot was submitted on Top.gg. * - * @since 2.0.0 + * @since 3.0.0 */ - time_t approved_at; + time_t submitted_at; /** * @brief The amount of votes this bot has. @@ -274,13 +274,13 @@ namespace topgg { TOPGG_BOT_QUERY_SORT(id, id); /** - * @brief Sorts results based on each bot's approval date. + * @brief Sorts results based on each bot's submit date. * * @return bot_query The current modified object. * @see topgg::client::get_bots - * @since 2.0.1 + * @since 3.0.0 */ - TOPGG_BOT_QUERY_SORT(approval_date, date); + TOPGG_BOT_QUERY_SORT(submit_date, date); /** * @brief Sorts results based on each bot's monthly vote count. @@ -309,7 +309,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_QUERY(uint16_t, skip, 499); + TOPGG_BOT_QUERY_QUERY(uint16_t, offset, 499); /** * @brief Queries only Discord bots that has this username. @@ -373,10 +373,10 @@ namespace topgg { * topgg_client * .get_bots() * .limit(250) - * .skip(50) + * .offset(50) * .username("shiro") * .sort_by_monthly_votes() - * .finish([](const auto& result) { + * .send([](const auto& result) { * try { * const auto bots = result.get(); * @@ -389,13 +389,13 @@ namespace topgg { * }); * ``` * - * @param callback The callback function to call when finish completes. - * @note For its C++20 coroutine counterpart, see co_finish. + * @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_finish + * @see topgg::bot_query::co_send( * @since 2.0.1 */ - void finish(const get_bots_completion_t& callback); + void send(const get_bots_completion_t& callback); #ifdef DPP_CORO /** @@ -411,10 +411,10 @@ namespace topgg { * const auto bots = co_await topgg_client * .get_bots() * .limit(250) - * .skip(50) + * .offset(50) * .username("shiro") * .sort_by_monthly_votes() - * .finish(); + * .send(); * * for (const auto& bot: bots) { * std::cout << topgg_bot.username << std::endl; @@ -432,10 +432,10 @@ namespace topgg { * @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::co_finish + * @see topgg::bot_query::co_send( * @since 2.0.1 */ - topgg::async_result> co_finish(); + topgg::async_result> co_send(); #endif friend class client; diff --git a/src/models.cpp b/src/models.cpp index ad698a2..31117da 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -87,15 +87,7 @@ account::account(const dpp::json& j) { id = dpp::snowflake{j["id"].template get()}; DESERIALIZE_ALIAS(j, username, name, std::string); - - try { - const auto hash{j["avatar"].template get()}; - const char* ext{hash.rfind("a_", 0) == 0 ? "gif" : "png"}; - - avatar = "https://cdn.discordapp.com/avatars/" + std::to_string(id) + "/" + hash + "." + ext + "?size=1024"; - } catch (TOPGG_UNUSED const std::exception&) { - avatar = "https://cdn.discordapp.com/embed/avatars/" + std::to_string((id >> 22) % 6) + ".png"; - } + DESERIALIZE(j, avatar, std::string); created_at = static_cast(((id >> 22) / 1000) + 1420070400); } @@ -121,11 +113,11 @@ bot::bot(const dpp::json& j) DESERIALIZE_OPTIONAL_STRING_ALIAS(j, bannerUrl, banner); - const auto j_approved_at{j["date"].template get()}; - tm approved_at_tm; + const auto j_submitted_at{j["date"].template get()}; + tm submitted_at_tm; - strptime(j_approved_at.data(), "%Y-%m-%dT%H:%M:%S", &approved_at_tm); - approved_at = mktime(&approved_at_tm); + strptime(j_submitted_at.data(), "%Y-%m-%dT%H:%M:%S", &submitted_at_tm); + submitted_at = mktime(&submitted_at_tm); DESERIALIZE_ALIAS(j, points, votes, size_t); DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); @@ -177,7 +169,7 @@ void bot_query::add_search(const char* key, const size_t value) { ADD_SEARCH(key, std::to_string(value)); } -void bot_query::finish(const topgg::get_bots_completion_t& callback) { +void bot_query::send(const topgg::get_bots_completion_t& callback) { if (m_sort != nullptr) { add_query("sort", m_sort); } @@ -200,7 +192,7 @@ void bot_query::finish(const topgg::get_bots_completion_t& callback) { } #ifdef DPP_CORO -dpp::async> bot_query::co_finish() { - return dpp::async>{ [this] (C&& cc) { return finish(std::forward(cc)); }}; +dpp::async> bot_query::co_send() { + return dpp::async>{ [this] (C&& cc) { return send(std::forward(cc)); }}; } #endif \ No newline at end of file diff --git a/test.cpp b/test.cpp index ce7e18f..39aa096 100644 --- a/test.cpp +++ b/test.cpp @@ -59,10 +59,10 @@ int main() { topgg_client .get_bots() .limit(250) - .skip(50) + .offset(50) .username("shiro") .sort_by_monthly_votes() - .finish(TEST_RESULT_CALLBACK()); + .send(TEST_RESULT_CALLBACK()); ACQUIRE_TEST_THREAD(); std::cout << "has_voted "; From e97194e588c9bac87a5068cd2f3242a48dd9ce2f Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 25 Feb 2025 15:13:48 +0700 Subject: [PATCH 18/41] feat: add topgg_id and id --- include/topgg/models.h | 87 +++++++++++++++++++++++++----------------- src/models.cpp | 30 +++++++++++---- 2 files changed, 74 insertions(+), 43 deletions(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index 8bdba80..e45d36d 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -46,66 +46,49 @@ } namespace topgg { + class bot_query; + class client; + /** - * @brief Base class of the account data stored in the Top.gg API. + * @brief Represents voters of a Discord bot. * - * @see topgg::bot - * @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 { public: - account() = delete; + voter() = delete; /** - * @brief This account's Discord ID. + * @brief This voter's Discord ID. * * @since 2.0.0 */ dpp::snowflake id; /** - * @brief This account's avatar URL. + * @brief This voter's avatar URL. * * @since 2.0.0 */ std::string avatar; /** - * @brief This account's username. + * @brief This voter's username. * * @since 3.0.0 */ std::string name; /** - * @brief When this account was created. + * @brief This voter's creation date. * * @since 2.0.0 */ time_t created_at; - }; - - class bot_query; - 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; + voter(const dpp::json& j); friend class client; }; @@ -114,15 +97,49 @@ namespace topgg { * @brief Represents a Discord bot listed on Top.gg. * * @see topgg::client::get_bot - * @see topgg::account * @since 2.0.0 */ - class TOPGG_EXPORT bot: public account { + class TOPGG_EXPORT bot { bot(const dpp::json& j); public: bot() = delete; + /** + * @brief This bot's Discord ID. + * + * @since 2.0.0 + */ + dpp::snowflake id; + + /** + * @brief This bot's Top.gg ID. + * + * @since 3.0.0 + */ + dpp::snowflake topgg_id; + + /** + * @brief This bot's avatar URL. + * + * @since 2.0.0 + */ + std::string avatar; + + /** + * @brief This bot's username. + * + * @since 3.0.0 + */ + std::string name; + + /** + * @brief This bot's creation date. + * + * @since 2.0.0 + */ + time_t created_at; + /** * @brief This bot's prefix. * @@ -181,7 +198,7 @@ namespace topgg { std::optional banner; /** - * @brief When this bot was submitted on Top.gg. + * @brief This bot's submission date. * * @since 3.0.0 */ @@ -274,13 +291,13 @@ namespace topgg { TOPGG_BOT_QUERY_SORT(id, id); /** - * @brief Sorts results based on each bot's submit date. + * @brief Sorts results based on each bot's submission date. * * @return bot_query The current modified object. * @see topgg::client::get_bots * @since 3.0.0 */ - TOPGG_BOT_QUERY_SORT(submit_date, date); + TOPGG_BOT_QUERY_SORT(submission_date, date); /** * @brief Sorts results based on each bot's monthly vote count. diff --git a/src/models.cpp b/src/models.cpp index 31117da..5ce40bd 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -1,8 +1,8 @@ #include -using topgg::account; using topgg::bot; using topgg::bot_query; +using topgg::voter; #ifdef _WIN32 #include @@ -71,6 +71,9 @@ static void strptime(const char* s, const char* f, tm* t) { } \ }) +#define SNOWFLAKE_FROM_JSON(j, name) \ + dpp::snowflake{j[#name].template get()} + #define ADD_QUERY(key, value) \ m_query.append(key); \ m_query.push_back('='); \ @@ -83,17 +86,19 @@ static void strptime(const char* s, const char* f, tm* t) { m_search.append(value); \ m_search.append("%20") -account::account(const dpp::json& j) { - id = dpp::snowflake{j["id"].template get()}; +static time_t timestamp_from_id(const dpp::snowflake& id) { + return static_cast(((id >> 22) / 1000) + 1420070400); +} + +bot::bot(const dpp::json& j): url("https://top.gg/bot/") { + id = SNOWFLAKE_FROM_JSON(j, clientid); + topgg_id = SNOWFLAKE_FROM_JSON(j, id); DESERIALIZE_ALIAS(j, username, name, std::string); DESERIALIZE(j, avatar, std::string); - created_at = static_cast(((id >> 22) / 1000) + 1420070400); -} + created_at = timestamp_from_id(id); -bot::bot(const dpp::json& j) - : account(j), url("https://top.gg/bot/") { DESERIALIZE(j, prefix, std::string); DESERIALIZE_ALIAS(j, shortdesc, short_description, std::string); DESERIALIZE_OPTIONAL_STRING_ALIAS(j, longdesc, long_description); @@ -195,4 +200,13 @@ void bot_query::send(const topgg::get_bots_completion_t& callback) { dpp::async> bot_query::co_send() { return dpp::async>{ [this] (C&& cc) { return send(std::forward(cc)); }}; } -#endif \ No newline at end of file +#endif + +voter::voter(const dpp::json& j) { + id = SNOWFLAKE_FROM_JSON(j, id); + + DESERIALIZE_ALIAS(j, username, name, std::string); + DESERIALIZE(j, avatar, std::string); + + created_at = timestamp_from_id(id); +} \ No newline at end of file From 612358a597e241fcd79e954221e5940e67df278f Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Wed, 26 Feb 2025 00:38:17 +0700 Subject: [PATCH 19/41] doc: simplify documentation --- include/topgg/models.h | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index e45d36d..7286e5a 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -260,7 +260,7 @@ namespace topgg { using get_bots_completion_t = std::function>&)>; /** - * @brief A class for configuring the query in get_bots before being sent to the Top.gg API. + * @brief A class for configuring the query in get_bots before being sent to the API. * * @see topgg::client::get_bots * @since 2.0.1 @@ -379,7 +379,7 @@ namespace topgg { TOPGG_BOT_QUERY_SEARCH(std::string&, vanity); /** - * @brief Sends the query to the Top.gg API. + * @brief Sends the query to the API. * * Example: * @@ -472,4 +472,4 @@ namespace topgg { #undef TOPGG_BOT_QUERY_SEARCH #undef TOPGG_BOT_QUERY_QUERY -#undef TOPGG_BOT_QUERY_SORT \ No newline at end of file +#undef TOPGG_BOT_QUERY_SORT From 8c7d1d19b7ad8a4a75a05c7fa2d7293e3526a83b Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 26 Feb 2025 12:09:14 +0700 Subject: [PATCH 20/41] feat: bot_query is now reusable --- include/topgg/models.h | 20 +++++++------- src/models.cpp | 60 ++++++++++++++++++++++++++---------------- 2 files changed, 47 insertions(+), 33 deletions(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index 7286e5a..de47551 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -12,7 +12,7 @@ #include -#include +#include #include #include #include @@ -267,11 +267,11 @@ namespace topgg { */ class TOPGG_EXPORT bot_query { client* m_client; - std::string m_query; - std::string m_search; + std::unordered_map m_query; + std::unordered_map m_search; const char* m_sort; - inline bot_query(client* c): m_client(c), m_query("/bots?"), m_sort(nullptr) {} + 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); void add_query(const char* key, const char* value); @@ -369,9 +369,9 @@ namespace topgg { TOPGG_BOT_QUERY_SEARCH(size_t, monthly_votes); /** - * @brief Queries only Discord bots that has this vanity URL. + * @brief Queries only Discord bots that has this Top.gg vanity URL. * - * @param vanity The bot's vanity URL. + * @param vanity The bot's Top.gg vanity URL. * @return bot_query The current modified object. * @see topgg::client::get_bots * @since 2.0.1 @@ -406,10 +406,10 @@ namespace topgg { * }); * ``` * - * @param callback The callback function to call when send( completes. - * @note For its C++20 coroutine counterpart, see co_send(. + * @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( + * @see topgg::bot_query::co_send * @since 2.0.1 */ void send(const get_bots_completion_t& callback); @@ -449,7 +449,7 @@ namespace topgg { * @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::co_send( + * @see topgg::bot_query::send * @since 2.0.1 */ topgg::async_result> co_send(); diff --git a/src/models.cpp b/src/models.cpp index 5ce40bd..8e04c56 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -74,18 +74,6 @@ static void strptime(const char* s, const char* f, tm* t) { #define SNOWFLAKE_FROM_JSON(j, name) \ dpp::snowflake{j[#name].template get()} -#define ADD_QUERY(key, value) \ - m_query.append(key); \ - m_query.push_back('='); \ - m_query.append(value); \ - m_query.push_back('&') - -#define ADD_SEARCH(key, value) \ - m_search.append(key); \ - m_search.append("%3A%20"); \ - m_search.append(value); \ - m_search.append("%20") - static time_t timestamp_from_id(const dpp::snowflake& id) { return static_cast(((id >> 22) / 1000) + 1420070400); } @@ -159,37 +147,63 @@ static std::string querystring(const std::string& value) { } void bot_query::add_query(const char* key, const uint16_t value, const uint16_t max) { - ADD_QUERY(key, std::to_string(std::min(value, max))); + m_query.insert_or_assign(key, std::to_string(std::min(value, max))); } void bot_query::add_query(const char* key, const char* value) { - ADD_QUERY(key, value); // querystring() not needed here + m_query.insert_or_assign(key, value); } void bot_query::add_search(const char* key, const std::string& value) { - ADD_SEARCH(key, querystring(value)); + m_search.insert_or_assign(key, querystring(value)); } void bot_query::add_search(const char* key, const size_t value) { - ADD_SEARCH(key, std::to_string(value)); + m_search.insert_or_assign(key, std::to_string(value)); } void bot_query::send(const topgg::get_bots_completion_t& callback) { + std::string path{"/bots?"}; + if (m_sort != nullptr) { - add_query("sort", m_sort); + path.append("sort="); + path.append(m_sort); + path.push_back('&'); + } + + std::string search{}; + + for (const auto& search_query: m_search) { + search.append("%20"); + search.append(search_query.first); + search.append("%3A%20"); + search.append(querystring(search_query.second)); + } + + const auto search_raw{search.c_str() + 3}; + + if (*search_raw != 0) { + path.append("search="); + path.append(search_raw); + path.push_back('&'); } - if (!m_search.empty()) { - add_query("search", m_search.c_str()); + for (const auto& additional_query: m_query) { + path.append(additional_query.first); + path.push_back('='); + path.append(additional_query.second); + path.push_back('&'); } - m_query.pop_back(); + path.pop_back(); - m_client->basic_request>(m_query, callback, [](const auto& j) { + m_client->basic_request>(path, callback, [](const auto& j) { std::vector bots{}; + + bots.reserve(j["count"].template get()); - for (const auto& part: j["results"].template get>()) { - bots.push_back(topgg::bot{part}); + for (const auto& bot: j["results"].template get>()) { + bots.push_back(topgg::bot{bot}); } return bots; From 2a1844eb9247e7edd2ca7229008369c477710997 Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Wed, 26 Feb 2025 12:17:48 +0700 Subject: [PATCH 21/41] fix: fix segmentation fault risk here --- src/models.cpp | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/models.cpp b/src/models.cpp index 8e04c56..51a3911 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -180,12 +180,14 @@ void bot_query::send(const topgg::get_bots_completion_t& callback) { search.append(querystring(search_query.second)); } - const auto search_raw{search.c_str() + 3}; + if (!search.empty()) { + const auto search_raw{search.c_str() + 3}; - if (*search_raw != 0) { - path.append("search="); - path.append(search_raw); - path.push_back('&'); + if (*search_raw != 0) { + path.append("search="); + path.append(search_raw); + path.push_back('&'); + } } for (const auto& additional_query: m_query) { @@ -223,4 +225,4 @@ voter::voter(const dpp::json& j) { DESERIALIZE(j, avatar, std::string); created_at = timestamp_from_id(id); -} \ No newline at end of file +} From 1adb4ac71fa9b30ee412f88ba3018369cc18d5e6 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 26 Feb 2025 13:29:55 +0700 Subject: [PATCH 22/41] fix: remove double encoding here --- include/topgg/models.h | 1 - src/models.cpp | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index de47551..2e6c98d 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -274,7 +274,6 @@ namespace topgg { 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); - void add_query(const char* key, const char* value); void add_search(const char* key, const std::string& value); void add_search(const char* key, const size_t value); diff --git a/src/models.cpp b/src/models.cpp index 8e04c56..32107c1 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -150,10 +150,6 @@ void bot_query::add_query(const char* key, const uint16_t value, const uint16_t m_query.insert_or_assign(key, std::to_string(std::min(value, max))); } -void bot_query::add_query(const char* key, const char* value) { - m_query.insert_or_assign(key, value); -} - void bot_query::add_search(const char* key, const std::string& value) { m_search.insert_or_assign(key, querystring(value)); } @@ -177,7 +173,7 @@ void bot_query::send(const topgg::get_bots_completion_t& callback) { search.append("%20"); search.append(search_query.first); search.append("%3A%20"); - search.append(querystring(search_query.second)); + search.append(search_query.second); } const auto search_raw{search.c_str() + 3}; From 4342ee01320a9443a8dc739c503a47e2d76c2840 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 26 Feb 2025 17:52:47 +0700 Subject: [PATCH 23/41] fix: fix get_bots --- include/topgg/client.h | 8 ++++---- include/topgg/models.h | 46 +++++++++++++++++++++--------------------- test.cpp | 4 ++-- 3 files changed, 29 insertions(+), 29 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 43d4e2c..2604d0d 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -209,8 +209,8 @@ namespace topgg { * topgg_client * .get_bots() * .limit(250) - * .offset(50) - * .username("shiro") + * .skip(50) + * .name("shiro") * .sort_by_monthly_votes() * .send([](const auto& result) { * try { @@ -235,8 +235,8 @@ namespace topgg { * const auto bots = co_await topgg_client * .get_bots() * .limit(250) - * .offset(50) - * .username("shiro") + * .skip(50) + * .name("shiro") * .sort_by_monthly_votes() * .send(); * diff --git a/include/topgg/models.h b/include/topgg/models.h index 2e6c98d..3285a8e 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -27,22 +27,22 @@ #undef _XOPEN_SOURCE #endif -#define TOPGG_BOT_QUERY_SORT(lib_name, api_name) \ - inline bot_query& sort_by_##lib_name() noexcept { \ - m_sort = #api_name; \ - return *this; \ +#define TOPGG_BOT_QUERY_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, name, ...) \ - inline bot_query& name(const type name) { \ - add_query(#name, name, __VA_ARGS__); \ - 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; \ } -#define TOPGG_BOT_QUERY_SEARCH(type, name, ...) \ - inline bot_query& name(const type name) { \ - add_search(#name, name, __VA_ARGS__); \ - return *this; \ +#define TOPGG_BOT_QUERY_SEARCH(type, lib_name, api_name, ...) \ + inline bot_query& lib_name(const type lib_name) { \ + add_search(#api_name, lib_name, __VA_ARGS__); \ + return *this; \ } namespace topgg { @@ -315,7 +315,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_QUERY(uint16_t, limit, 500); + TOPGG_BOT_QUERY_QUERY(uint16_t, limit, limit, 500); /** * @brief Sets the amount of bots to be skipped during the query. @@ -325,7 +325,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_QUERY(uint16_t, offset, 499); + TOPGG_BOT_QUERY_QUERY(uint16_t, skip, offset, 499); /** * @brief Queries only Discord bots that has this username. @@ -335,7 +335,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_SEARCH(std::string&, username); + TOPGG_BOT_QUERY_SEARCH(std::string&, name, username); /** * @brief Queries only Discord bots that has this prefix. @@ -345,7 +345,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_SEARCH(std::string&, prefix); + TOPGG_BOT_QUERY_SEARCH(std::string&, prefix, prefix); /** * @brief Queries only Discord bots that has this vote count. @@ -355,7 +355,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_SEARCH(size_t, votes); + TOPGG_BOT_QUERY_SEARCH(size_t, votes, points); /** * @brief Queries only Discord bots that has this monthly vote count. @@ -365,7 +365,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_SEARCH(size_t, monthly_votes); + TOPGG_BOT_QUERY_SEARCH(size_t, monthly_votes, monthlyPoints); /** * @brief Queries only Discord bots that has this Top.gg vanity URL. @@ -375,7 +375,7 @@ namespace topgg { * @see topgg::client::get_bots * @since 2.0.1 */ - TOPGG_BOT_QUERY_SEARCH(std::string&, vanity); + TOPGG_BOT_QUERY_SEARCH(std::string&, vanity, vanity); /** * @brief Sends the query to the API. @@ -389,8 +389,8 @@ namespace topgg { * topgg_client * .get_bots() * .limit(250) - * .offset(50) - * .username("shiro") + * .skip(50) + * .name("shiro") * .sort_by_monthly_votes() * .send([](const auto& result) { * try { @@ -427,8 +427,8 @@ namespace topgg { * const auto bots = co_await topgg_client * .get_bots() * .limit(250) - * .offset(50) - * .username("shiro") + * .skip(50) + * .name("shiro") * .sort_by_monthly_votes() * .send(); * diff --git a/test.cpp b/test.cpp index 39aa096..e935584 100644 --- a/test.cpp +++ b/test.cpp @@ -59,8 +59,8 @@ int main() { topgg_client .get_bots() .limit(250) - .offset(50) - .username("shiro") + .skip(50) + .name("shiro") .sort_by_monthly_votes() .send(TEST_RESULT_CALLBACK()); From 15634e6cc983e84bddba1eaf680f2d6e45ac19c3 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 26 Feb 2025 18:18:50 +0700 Subject: [PATCH 24/41] feat: add review_score and review_count --- include/topgg/models.h | 14 ++++++++++++++ src/models.cpp | 5 +++++ 2 files changed, 19 insertions(+) diff --git a/include/topgg/models.h b/include/topgg/models.h index 3285a8e..c9fe9c2 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -246,6 +246,20 @@ namespace topgg { */ std::optional server_count; + /** + * @brief This bot's average review score out of 5. + * + * @since 3.0.0 + */ + double review_score; + + /** + * @brief This bot's review count. + * + * @since 3.0.0 + */ + size_t review_count; + friend class bot_query; friend class client; }; diff --git a/src/models.cpp b/src/models.cpp index 1fcf553..96d1517 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -123,6 +123,11 @@ bot::bot(const dpp::json& j): url("https://top.gg/bot/") { } catch (TOPGG_UNUSED const std::exception&) { url.append(std::to_string(id)); } + + const auto reviews{j["reviews"]}; + + DESERIALIZE_ALIAS(reviews, averageScore, review_score, double); + DESERIALIZE_ALIAS(reviews, count, review_count, size_t); } static std::string querystring(const std::string& value) { From 0a56eee45d67d642de6135c1c36f453102436ec0 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 27 Feb 2025 20:48:16 +0700 Subject: [PATCH 25/41] refactor: change __TOPGG_TESTING__ usage --- src/client.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index 2c223d9..cac2dc5 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -106,6 +106,9 @@ topgg::async_result client::co_get_bot(const dpp::snowflake bot_id) #endif size_t client::get_server_count() { +#ifdef __TOPGG_TESTING__ + return 2; +#else size_t server_count{}; for (auto& s: m_cluster.get_shards()) { @@ -113,6 +116,7 @@ size_t client::get_server_count() { } return server_count; +#endif } void client::post_server_count_inner(const size_t server_count, dpp::http_completion_event callback) { @@ -128,11 +132,7 @@ void client::post_server_count(const topgg::post_server_count_completion_t& call const auto server_count{get_server_count()}; if (server_count == 0) { -#ifdef __TOPGG_TESTING__ - return callback(true); -#else return callback(false); -#endif } post_server_count_inner(server_count, [callback](const auto& response) { From 55b716b875157605cf50056b3cd7c5e9d5182eab Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 27 Feb 2025 22:36:48 +0700 Subject: [PATCH 26/41] feat: add test_autoposter and autoposter callbacks --- include/topgg/client.h | 102 ++++++++++++++++++++++++++++++++++------- include/topgg/models.h | 4 +- include/topgg/topgg.h | 6 +++ src/client.cpp | 62 ++++++++++++++++++------- src/models.cpp | 2 +- test.cpp | 4 ++ test_autoposter.cpp | 55 ++++++++++++++++++++++ 7 files changed, 199 insertions(+), 36 deletions(-) create mode 100644 test_autoposter.cpp diff --git a/include/topgg/client.h b/include/topgg/client.h index 2604d0d..0490f6c 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -24,7 +24,7 @@ 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_server_count completes. @@ -32,7 +32,7 @@ namespace topgg { * @see topgg::client::get_server_count * @since 3.0.0 */ - using get_server_count_completion_t = std::function>&)>; + using get_server_count_completion_event = std::function>&)>; /** * @brief The callback function to call when get_voters completes. @@ -40,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. @@ -48,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. @@ -56,7 +56,7 @@ 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_server_count completes. @@ -64,7 +64,16 @@ namespace topgg { * @see topgg::client::post_server_count * @since 3.0.0 */ - using post_server_count_completion_t = std::function; + using post_server_count_completion_event = std::function; + + /** + * @brief The callback function to call after every request to the API, successful or not. + * + * @see topgg::client::start_autoposter + * @see topgg::client::stop_autoposter + * @since 3.0.0 + */ + using autopost_completion_event = std::function&)>; /** * @brief Main client class that lets you make HTTP requests with the Top.gg API. @@ -160,7 +169,7 @@ 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 /** @@ -283,7 +292,7 @@ namespace topgg { * @see topgg::client::co_get_server_count * @since 3.0.0 */ - void get_server_count(const get_server_count_completion_t& callback); + void get_server_count(const get_server_count_completion_event& callback); #ifdef DPP_CORO /** @@ -349,7 +358,7 @@ namespace topgg { * @see topgg::client::co_get_voters * @since 2.0.0 */ - void get_voters(const get_voters_completion_t& callback); + void get_voters(const get_voters_completion_event& callback); #ifdef DPP_CORO /** @@ -416,7 +425,7 @@ namespace topgg { * @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 /** @@ -481,7 +490,7 @@ 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 /** @@ -541,7 +550,7 @@ namespace topgg { * @see topgg::client::co_post_server_count * @since 3.0.0 */ - void post_server_count(const post_server_count_completion_t& callback); + void post_server_count(const post_server_count_completion_event& callback); #ifdef DPP_CORO /** @@ -569,6 +578,32 @@ namespace topgg { dpp::async co_post_server_count(); #endif + /** + * @brief Starts autoposting your 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"}; + * + * bot.start_autoposter([](const auto& result) { + * if (result) { + * std::cout << "Successfully posted " << *result << " servers to the Top.gg API!" << std::endl; + * } + * }); + * ``` + * + * @param callback The callback function to call after every request to the API, successful or not. + * @param delay The minimum delay between post requests 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 start_autoposter(const autopost_completion_event& callback, time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); + /** * @brief Starts autoposting your bot's server count using data directly from your D++ cluster instance. * @@ -587,8 +622,43 @@ namespace topgg { * @see topgg::client::stop_autoposter * @since 2.0.0 */ - void start_autoposter(time_t delay = 900); - + void start_autoposter(time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); + + /** + * @brief Starts autoposting your 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.start_autoposter(reinterpret_cast(new my_autoposter_source), [](const auto& result) { + * if (result) { + * std::cout << "Successfully posted " << *result << " servers to the Top.gg API!" << std::endl; + * } + * }); + * ``` + * + * @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 delay The minimum delay between post requests 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 + * @see topgg::autoposter_source + * @since 2.0.0 + */ + void start_autoposter(autoposter_source* source, const autopost_completion_event& callback, time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); + /** * @brief Starts autoposting your bot's server count using a custom data source. * @@ -611,12 +681,12 @@ namespace topgg { * @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 delay The minimum delay between post requests in seconds. Defaults to 15 minutes. * @note This function has no effect if the autoposter is already running. - * @see topgg::client::autoposter_source * @see topgg::client::post_server_count * @see topgg::client::stop_autoposter + * @see topgg::autoposter_source * @since 3.0.0 */ - void start_autoposter(autoposter_source* source, time_t delay = 900); + void start_autoposter(autoposter_source* source, time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); /** * @brief Prematurely stops the autoposter. Calling this function is usually unnecessary as this function is called later in the destructor. diff --git a/include/topgg/models.h b/include/topgg/models.h index c9fe9c2..5434bfb 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -271,7 +271,7 @@ namespace topgg { * @see topgg::bot_query * @since 2.0.1 */ - using get_bots_completion_t = std::function>&)>; + using get_bots_completion_event = std::function>&)>; /** * @brief A class for configuring the query in get_bots before being sent to the API. @@ -425,7 +425,7 @@ namespace topgg { * @see topgg::bot_query::co_send * @since 2.0.1 */ - void send(const get_bots_completion_t& callback); + void send(const get_bots_completion_event& callback); #ifdef DPP_CORO /** diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index 85bc83a..e8039ec 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -35,6 +35,12 @@ #define TOPGG_UNUSED #endif +#ifdef __TOPGG_TESTING__ +#define TOPGG_AUTOPOSTER_MIN_DELAY 5 +#else +#define TOPGG_AUTOPOSTER_MIN_DELAY 900 +#endif + #ifdef __clang__ #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunknown-warning-option" diff --git a/src/client.cpp b/src/client.cpp index cac2dc5..8899917 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -93,7 +93,7 @@ client::client(dpp::cluster& cluster, const std::string& token): m_token(token), 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}; }); @@ -128,7 +128,7 @@ void client::post_server_count_inner(const size_t server_count, dpp::http_comple m_cluster.request(TOPGG_BASE_URL "/bots/" + m_id + "/stats", dpp::m_post, callback, j.dump(), "application/json", headers); } -void client::post_server_count(const topgg::post_server_count_completion_t& callback) { +void client::post_server_count(const topgg::post_server_count_completion_event& callback) { const auto server_count{get_server_count()}; if (server_count == 0) { @@ -146,7 +146,7 @@ dpp::async client::co_post_server_count() { } #endif -void client::get_server_count(const topgg::get_server_count_completion_t& callback) { +void client::get_server_count(const topgg::get_server_count_completion_event& callback) { basic_request>("/bots/" + m_id + "/stats", callback, [](const auto& j) { std::optional server_count{}; @@ -164,7 +164,7 @@ topgg::async_result> client::co_get_server_count() { } #endif -void client::get_voters(const topgg::get_voters_completion_t& callback) { +void client::get_voters(const topgg::get_voters_completion_event& callback) { basic_request>("/bots/" + m_id + "/votes", callback, [](const auto& j) { std::vector voters{}; @@ -183,7 +183,7 @@ topgg::async_result> client::co_get_voters() { #endif -void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_completion_t& callback) { +void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_completion_event& callback) { basic_request("/bots/" + m_id + "/check?userId=" + std::to_string(user_id), callback, [](const auto& j) { return j["voted"].template get() != 0; }); @@ -195,7 +195,7 @@ topgg::async_result client::co_has_voted(const dpp::snowflake user_id) { } #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(); }); @@ -207,36 +207,64 @@ topgg::async_result client::co_is_weekend() { } #endif -void client::start_autoposter(time_t delay) { - if (delay < 900) { - delay = 900; +void client::start_autoposter(const topgg::autopost_completion_event& callback, time_t delay) { + if (delay < TOPGG_AUTOPOSTER_MIN_DELAY) { + delay = TOPGG_AUTOPOSTER_MIN_DELAY; } /** * 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. */ - else if (!m_autoposter_timer) { - m_autoposter_timer = m_cluster.start_timer([this](TOPGG_UNUSED dpp::timer) { - post_server_count_inner(get_server_count(), [](TOPGG_UNUSED const auto&) {}); + if (!m_autoposter_timer) { + m_autoposter_timer = m_cluster.start_timer([this, callback](TOPGG_UNUSED dpp::timer) { + 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); + } + }); + } }, delay); } } -void client::start_autoposter(topgg::autoposter_source* source, time_t delay) { +void client::start_autoposter(const time_t delay) { + start_autoposter([](TOPGG_UNUSED const auto&) {}, delay); +} + +void client::start_autoposter(topgg::autoposter_source* source, const topgg::autopost_completion_event& callback, time_t delay) { if (!m_autoposter_timer) { - if (delay < 900) { - delay = 900; + if (delay < TOPGG_AUTOPOSTER_MIN_DELAY) { + delay = TOPGG_AUTOPOSTER_MIN_DELAY; } - m_autoposter_timer = m_cluster.start_timer([this, source](TOPGG_UNUSED dpp::timer) { - post_server_count_inner(source->get_server_count(m_cluster), [](TOPGG_UNUSED const auto&) {}); + 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); + } + }); + } }, delay, [source](TOPGG_UNUSED dpp::timer) { delete source; }); } } +void client::start_autoposter(topgg::autoposter_source* source, time_t delay) { + start_autoposter(source, [](TOPGG_UNUSED const auto&) {}, delay); +} + 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 96d1517..56c8f0b 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -163,7 +163,7 @@ void bot_query::add_search(const char* key, const size_t value) { m_search.insert_or_assign(key, std::to_string(value)); } -void bot_query::send(const topgg::get_bots_completion_t& callback) { +void bot_query::send(const topgg::get_bots_completion_event& callback) { std::string path{"/bots?"}; if (m_sort != nullptr) { diff --git a/test.cpp b/test.cpp index e935584..3131f8a 100644 --- a/test.cpp +++ b/test.cpp @@ -1,3 +1,7 @@ +#ifndef __TOPGG_TESTING__ +#define __TOPGG_TESTING__ +#endif + #include #include diff --git a/test_autoposter.cpp b/test_autoposter.cpp new file mode 100644 index 0000000..a496acf --- /dev/null +++ b/test_autoposter.cpp @@ -0,0 +1,55 @@ +#ifndef __TOPGG_TESTING__ +#define __TOPGG_TESTING__ +#endif + +#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 topgg_client{bot, topgg_token}; + + std::cout << "starting bot "; + + bot.start(dpp::start_type::st_return); + + std::cout << "ok\n"; + + topgg_client.start_autoposter([](const auto& result) { + if (result) { + std::cout << "Successfully posted " << *result << " servers to the Top.gg 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; +} From a748cc9f344e7965934d289eee0bd4ae23fe32d7 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 4 Mar 2025 20:56:06 +0700 Subject: [PATCH 27/41] feat: add page to get_voters --- include/topgg/client.h | 40 +++++++++++++++++++++++++++++++++++++--- src/client.cpp | 17 ++++++++++++----- 2 files changed, 49 insertions(+), 8 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 0490f6c..b0f9b23 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -329,7 +329,40 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot's last 1000 unique voters. + * @brief Fetches your Discord bot's recent unique voters. + * + * Example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client topgg_client{bot, "your top.gg token"}; + * + * topgg_client.get_voters(1, [](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 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::client::start_autoposter + * @see topgg::client::co_get_voters + * @since 2.0.0 + */ + void get_voters(size_t page, const get_voters_completion_event& callback); + + /** + * @brief Fetches your Discord bot's recent 100 unique voters. * * Example: * @@ -362,7 +395,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot's last 1000 unique voters through a C++20 coroutine. + * @brief Fetches your Discord bot's recent unique voters through a C++20 coroutine. * * Example: * @@ -381,6 +414,7 @@ namespace topgg { * } * ``` * + * @param page The page number. Each page can only have at most 100 voters. * @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. @@ -394,7 +428,7 @@ namespace topgg { * @see topgg::client::get_voters * @since 2.0.0 */ - topgg::async_result> co_get_voters(); + topgg::async_result> co_get_voters(size_t page = 1); #endif /** diff --git a/src/client.cpp b/src/client.cpp index 8899917..93c6e97 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -164,8 +164,12 @@ topgg::async_result> client::co_get_server_count() { } #endif -void client::get_voters(const topgg::get_voters_completion_event& callback) { - basic_request>("/bots/" + m_id + "/votes", callback, [](const auto& j) { +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) { @@ -176,13 +180,16 @@ void client::get_voters(const topgg::get_voters_completion_event& 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_event& callback) { basic_request("/bots/" + m_id + "/check?userId=" + std::to_string(user_id), callback, [](const auto& j) { return j["voted"].template get() != 0; From e0158ddd0d1c4e54654903026c3b9917715e10cc Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 4 Mar 2025 21:00:39 +0700 Subject: [PATCH 28/41] doc: reword documentation --- CMakeLists.txt | 2 +- README.md | 22 +++--- docs/Doxyfile | 2 +- docs/header.html | 4 +- include/topgg/client.h | 162 ++++++++++++++++++++--------------------- include/topgg/models.h | 75 +++++++++---------- include/topgg/result.h | 101 +++++++++++++------------ include/topgg/topgg.h | 6 +- src/client.cpp | 59 ++++++++------- src/models.cpp | 9 ++- test.cpp | 2 +- test_autoposter.cpp | 4 +- topgg.rc | 2 +- 13 files changed, 228 insertions(+), 222 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 1a10848..6cad2e9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ 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_VERBOSE_MAKEFILE ON) diff --git a/README.md b/README.md index f44ccd1..a9fabd4 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Top.gg SDK for C++ -The official C++ SDK for the [Top.gg API](https://docs.top.gg). +A simple API wrapper for [Top.gg](https://top.gg) written in C++. ## Building from source @@ -58,7 +58,7 @@ cmake --build build --config Release dpp::cluster bot{"your bot token"}; topgg::client topgg_client{bot, "your top.gg token"}; -// using C++17 callbacks +// Using C++17 callbacks topgg_client.get_bot(264811613708746752, [](const auto& result) { try { const auto topgg_bot = result.get(); @@ -69,7 +69,7 @@ topgg_client.get_bot(264811613708746752, [](const auto& result) { } }); -// using C++20 coroutines +// Using C++20 coroutines try { const auto topgg_bot = co_await topgg_client.co_get_bot(264811613708746752); @@ -85,18 +85,18 @@ try { dpp::cluster bot{"your bot token"}; topgg::client topgg_client{bot, "your top.gg token"}; -// using C++17 callbacks +// Using C++17 callbacks topgg_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 +// Using C++20 coroutines const auto success = co_await topgg_client.co_post_server_count(); if (success) { - std::cout << "stats posted!" << std::endl; + std::cout << "Stats posted!" << std::endl; } ``` @@ -106,23 +106,23 @@ if (success) { dpp::cluster bot{"your bot token"}; topgg::client topgg_client{bot, "your top.gg token"}; -// using C++17 callbacks +// Using C++17 callbacks topgg_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::cerr << "error: " << exc.what() << std::endl; } }); -// using C++20 coroutines +// Using C++20 coroutines try { const auto voted = co_await topgg_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::cerr << "error: " << exc.what() << std::endl; diff --git a/docs/Doxyfile b/docs/Doxyfile index 7a947fd..aa24b09 100644 --- a/docs/Doxyfile +++ b/docs/Doxyfile @@ -54,7 +54,7 @@ PROJECT_NUMBER = 3.0.0 # 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 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/include/topgg/client.h b/include/topgg/client.h index b0f9b23..7967d3f 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -1,7 +1,7 @@ /** * @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-2025 Top.gg & null8626 * @date 2025-02-25 @@ -57,7 +57,7 @@ namespace topgg { * @since 2.0.0 */ using is_weekend_completion_event = std::function&)>; - + /** * @brief The callback function to call when post_server_count completes. * @@ -67,16 +67,16 @@ namespace topgg { using post_server_count_completion_event = std::function; /** - * @brief The callback function to call after every request to the API, successful or not. + * @brief The callback function to call after every autopost request to the API, successful or not. * * @see topgg::client::start_autoposter * @see topgg::client::stop_autoposter * @since 3.0.0 */ 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 */ @@ -94,7 +94,7 @@ namespace topgg { size_t get_server_count(); void post_server_count_inner(const size_t server_count, dpp::http_completion_event callback); - + public: client() = delete; @@ -102,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); @@ -142,7 +142,7 @@ 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: * @@ -161,7 +161,7 @@ namespace topgg { * }); * ``` * - * @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 @@ -173,7 +173,7 @@ namespace topgg { #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: * @@ -190,12 +190,12 @@ namespace topgg { * } * ``` * - * @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 has 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 Such query 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 @@ -207,7 +207,7 @@ namespace topgg { #endif /** - * @brief Queries/searches through the Top.gg database to look for matching listed Discord bots. + * @brief Returns an object that allows you to configure a bot query before sending it to the API. * * C++17 example: * @@ -224,7 +224,7 @@ namespace topgg { * .send([](const auto& result) { * try { * const auto bots = result.get(); - * + * * for (const auto& bot: bots) { * std::cout << bot.username << std::endl; * } @@ -233,9 +233,9 @@ namespace topgg { * } * }); * ``` - * + * * C++20 example: - * + * * ```cpp * dpp::cluster bot{"your bot token"}; * topgg::client topgg_client{bot, "your top.gg token"}; @@ -256,8 +256,8 @@ namespace topgg { * std::cerr << "error: " << exc.what() << std::endl; * } * ``` - * - * @return bot_query An object for configuring the query in get_bots before being sent to the Top.gg API. + * + * @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 */ @@ -313,11 +313,11 @@ namespace topgg { * } * ``` * - * @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 has occured. + * @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 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 @@ -329,7 +329,7 @@ namespace topgg { #endif /** - * @brief Fetches your Discord bot's recent unique voters. + * @brief Fetches your Discord bot's recent 100 unique voters. * * Example: * @@ -395,7 +395,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot's recent unique voters through a C++20 coroutine. + * @brief Fetches your Discord bot's recent 100 unique voters through a C++20 coroutine. * * Example: * @@ -414,12 +414,12 @@ namespace topgg { * } * ``` * - * @param page The page number. Each page can only have at most 100 voters. - * @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 has occured. + * @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::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 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 @@ -432,7 +432,7 @@ namespace topgg { #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: * @@ -443,7 +443,7 @@ namespace topgg { * topgg_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::cerr << "error: " << exc.what() << std::endl; @@ -451,7 +451,7 @@ namespace topgg { * }); * ``` * - * @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 @@ -463,7 +463,7 @@ namespace topgg { #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: * @@ -475,19 +475,19 @@ namespace topgg { * const auto voted = co_await topgg_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::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 has 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 Such query 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 bool if successful * @note For its C++17 callback-based counterpart, see has_voted. * @see topgg::async_result @@ -499,7 +499,7 @@ 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: * @@ -510,7 +510,7 @@ namespace topgg { * topgg_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::cerr << "error: " << exc.what() << std::endl; @@ -528,7 +528,7 @@ namespace topgg { #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: * @@ -540,18 +540,18 @@ namespace topgg { * const auto is_weekend = co_await topgg_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::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 has occured. + * @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 co_await to retrieve a bool if successful * @note For its C++17 callback-based counterpart, see is_weekend. * @see topgg::async_result @@ -562,7 +562,7 @@ namespace topgg { #endif /** - * @brief Manually posts your Discord bot's server count 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: * @@ -572,7 +572,7 @@ namespace topgg { * * topgg_client.post_server_count([](const auto success) { * if (success) { - * std::cout << "stats posted!" << std::endl; + * std::cout << "Stats posted!" << std::endl; * } * }); * ``` @@ -588,7 +588,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Manually posts your Discord bot's server count 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: * @@ -599,7 +599,7 @@ namespace topgg { * const auto success = co_await topgg_client.co_post_server_count(); * * if (success) { - * std::cout << "stats posted!" << std::endl; + * std::cout << "Stats posted!" << std::endl; * } * ``` * @@ -613,7 +613,7 @@ namespace topgg { #endif /** - * @brief Starts autoposting your bot's server count using data directly from your D++ cluster instance. + * @brief Starts autoposting your Discord bot's server count using data directly from your D++ cluster instance. * * Example: * @@ -623,23 +623,23 @@ namespace topgg { * * bot.start_autoposter([](const auto& result) { * if (result) { - * std::cout << "Successfully posted " << *result << " servers to the Top.gg API!" << std::endl; + * std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; * } * }); * ``` * * @param callback The callback function to call after every request to the API, successful or not. - * @param delay The minimum delay between post requests in seconds. Defaults to 15 minutes. + * @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 start_autoposter(const autopost_completion_event& callback, time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); + void start_autoposter(const autopost_completion_event& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); /** - * @brief Starts autoposting your bot's server count using data directly from your D++ cluster instance. + * @brief Starts autoposting your Discord bot's server count using data directly from your D++ cluster instance. * * Example: * @@ -650,16 +650,16 @@ namespace topgg { * bot.start_autoposter(); * ``` * - * @param delay The minimum delay between post requests in seconds. Defaults to 15 minutes. + * @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 */ - void start_autoposter(time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); + void start_autoposter(time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); /** - * @brief Starts autoposting your bot's server count using a custom data source. + * @brief Starts autoposting your Discord bot's server count using a custom data source. * * Example: * @@ -670,20 +670,20 @@ namespace topgg { * return ...; * } * }; - * + * * dpp::cluster bot{"your bot token"}; * topgg::client topgg_client{bot, "your top.gg token"}; - * + * * topgg_client.start_autoposter(reinterpret_cast(new my_autoposter_source), [](const auto& result) { * if (result) { - * std::cout << "Successfully posted " << *result << " servers to the Top.gg API!" << std::endl; + * std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; * } * }); * ``` * * @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 delay The minimum delay between post requests in seconds. Defaults to 15 minutes. + * @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 @@ -691,10 +691,10 @@ namespace topgg { * @see topgg::autoposter_source * @since 2.0.0 */ - void start_autoposter(autoposter_source* source, const autopost_completion_event& callback, time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); + void start_autoposter(autoposter_source* source, const autopost_completion_event& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); /** - * @brief Starts autoposting your bot's server count using a custom data source. + * @brief Starts autoposting your Discord bot's server count using a custom data source. * * Example: * @@ -705,25 +705,25 @@ namespace topgg { * return ...; * } * }; - * + * * dpp::cluster bot{"your bot token"}; * topgg::client topgg_client{bot, "your top.gg token"}; - * + * * topgg_client.start_autoposter(reinterpret_cast(new my_autoposter_source)); * ``` * * @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 delay The minimum delay between post requests in seconds. Defaults to 15 minutes. + * @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::autoposter_source * @since 3.0.0 */ - void start_autoposter(autoposter_source* source, time_t delay = TOPGG_AUTOPOSTER_MIN_DELAY); - + 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: * @@ -743,7 +743,7 @@ namespace topgg { * @since 2.0.0 */ void stop_autoposter() noexcept; - + /** * @brief The destructor. Stops the autoposter if it's running. */ @@ -751,4 +751,4 @@ namespace topgg { friend class bot_query; }; -}; // namespace topgg \ No newline at end of file +}; // namespace topgg diff --git a/include/topgg/models.h b/include/topgg/models.h index 5434bfb..7ab84dd 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -1,7 +1,7 @@ /** * @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-2025 Top.gg & null8626 * @date 2025-02-25 @@ -50,7 +50,7 @@ namespace topgg { class client; /** - * @brief Represents voters of a Discord bot. + * @brief A Top.gg voter. * * @see topgg::client::get_voters * @see topgg::client::start_autoposter @@ -94,7 +94,7 @@ namespace topgg { }; /** - * @brief Represents a Discord bot listed on Top.gg. + * @brief A Discord bot listed on Top.gg. * * @see topgg::client::get_bot * @since 2.0.0 @@ -184,7 +184,7 @@ namespace topgg { std::optional github; /** - * @brief This bot's owners IDs. + * @brief This bot's owner IDs. * * @since 2.0.0 */ @@ -248,14 +248,14 @@ namespace topgg { /** * @brief This bot's average review score out of 5. - * + * * @since 3.0.0 */ double review_score; /** * @brief This bot's review count. - * + * * @since 3.0.0 */ size_t review_count; @@ -274,7 +274,7 @@ namespace topgg { using get_bots_completion_event = std::function>&)>; /** - * @brief A class for configuring the query in get_bots before being sent to the API. + * @brief Configure a Discord bot query before sending it to the API. * * @see topgg::client::get_bots * @since 2.0.1 @@ -284,9 +284,10 @@ namespace topgg { std::unordered_map m_query; std::unordered_map m_search; const char* m_sort; - - inline bot_query(client* c): m_client(c), m_sort(nullptr) {} - + + 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); void add_search(const char* key, const std::string& value); void add_search(const char* key, const size_t value); @@ -296,7 +297,7 @@ namespace topgg { /** * @brief Sorts results based on each bot's ID. - * + * * @return bot_query The current modified object. * @see topgg::client::get_bots * @since 2.0.1 @@ -305,7 +306,7 @@ namespace topgg { /** * @brief Sorts results based on each bot's submission date. - * + * * @return bot_query The current modified object. * @see topgg::client::get_bots * @since 3.0.0 @@ -314,7 +315,7 @@ namespace topgg { /** * @brief Sorts results based on each bot's monthly vote count. - * + * * @return bot_query The current modified object. * @see topgg::client::get_bots * @since 2.0.1 @@ -323,7 +324,7 @@ namespace topgg { /** * @brief Sets the maximum amount of bots to be queried. - * + * * @param limit The maximum amount of bots to be queried. This cannot be more than 500. * @return bot_query The current modified object. * @see topgg::client::get_bots @@ -332,9 +333,9 @@ namespace topgg { TOPGG_BOT_QUERY_QUERY(uint16_t, limit, limit, 500); /** - * @brief Sets the amount of bots to be skipped during the query. - * - * @param skip The amount of bots to be skipped during the query. This cannot be more than 499. + * @brief Sets the amount of bots to be skipped. + * + * @param skip The amount of bots to be skipped. This cannot be more than 499. * @return bot_query The current modified object. * @see topgg::client::get_bots * @since 2.0.1 @@ -342,8 +343,8 @@ namespace topgg { TOPGG_BOT_QUERY_QUERY(uint16_t, skip, offset, 499); /** - * @brief Queries only Discord bots that has this username. - * + * @brief Queries only bots that has this username. + * * @param username The bot's username. * @return bot_query The current modified object. * @see topgg::client::get_bots @@ -352,8 +353,8 @@ namespace topgg { TOPGG_BOT_QUERY_SEARCH(std::string&, name, username); /** - * @brief Queries only Discord bots that has this prefix. - * + * @brief Queries only bots that has this prefix. + * * @param prefix The bot's prefix. * @return bot_query The current modified object. * @see topgg::client::get_bots @@ -362,8 +363,8 @@ namespace topgg { TOPGG_BOT_QUERY_SEARCH(std::string&, prefix, prefix); /** - * @brief Queries only Discord bots that has this vote count. - * + * @brief Queries only bots that has this vote count. + * * @param votes The bot's vote count. * @return bot_query The current modified object. * @see topgg::client::get_bots @@ -372,8 +373,8 @@ namespace topgg { TOPGG_BOT_QUERY_SEARCH(size_t, votes, points); /** - * @brief Queries only Discord bots that has this monthly vote count. - * + * @brief Queries only bots that has this monthly vote count. + * * @param monthly_votes The bot's monthly vote count. * @return bot_query The current modified object. * @see topgg::client::get_bots @@ -382,15 +383,15 @@ namespace topgg { TOPGG_BOT_QUERY_SEARCH(size_t, monthly_votes, monthlyPoints); /** - * @brief Queries only Discord bots that has this Top.gg vanity URL. - * + * @brief Queries only bots that has this Top.gg vanity URL. + * * @param vanity The bot's Top.gg vanity URL. * @return bot_query The current modified object. * @see topgg::client::get_bots * @since 2.0.1 */ TOPGG_BOT_QUERY_SEARCH(std::string&, vanity, vanity); - + /** * @brief Sends the query to the API. * @@ -409,7 +410,7 @@ namespace topgg { * .send([](const auto& result) { * try { * const auto bots = result.get(); - * + * * for (const auto& bot: bots) { * std::cout << bot.username << std::endl; * } @@ -418,7 +419,7 @@ namespace topgg { * } * }); * ``` - * + * * @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 @@ -429,7 +430,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Sends the query to the Top.gg API through a C++20 coroutine. + * @brief Sends the query to the API through a C++20 coroutine. * * Example: * @@ -453,12 +454,12 @@ namespace topgg { * 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 has occured. + * + * @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 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 diff --git a/include/topgg/result.h b/include/topgg/result.h index 5b456ae..97bad68 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -1,7 +1,7 @@ /** * @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-2025 Top.gg & null8626 * @date 2025-02-25 @@ -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,17 +67,17 @@ 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; @@ -95,12 +95,11 @@ 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 @@ -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 has 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 has 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 has 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 has 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 e8039ec..80c9f91 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -1,7 +1,7 @@ /** * @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-2025 Top.gg & null8626 * @date 2025-02-25 @@ -36,9 +36,9 @@ #endif #ifdef __TOPGG_TESTING__ -#define TOPGG_AUTOPOSTER_MIN_DELAY 5 +#define TOPGG_AUTOPOSTER_MIN_INTERVAL 5 #else -#define TOPGG_AUTOPOSTER_MIN_DELAY 900 +#define TOPGG_AUTOPOSTER_MIN_INTERVAL 900 #endif #ifdef __clang__ diff --git a/src/client.cpp b/src/client.cpp index 93c6e97..19e1308 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -2,6 +2,7 @@ using topgg::client; +// 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, @@ -19,6 +20,7 @@ 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 }; +// clang-format on static bool base64_decode(const std::string& input, std::string& output) { const auto input_size{input.size()}; @@ -28,11 +30,11 @@ static bool base64_decode(const std::string& input, std::string& output) { } 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--; } @@ -72,11 +74,11 @@ static std::string id_from_bot_token(std::string bot_token) { 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; } @@ -85,7 +87,8 @@ static std::string id_from_bot_token(std::string bot_token) { throw std::invalid_argument{"Got a malformed Discord Bot token."}; } -client::client(dpp::cluster& cluster, const std::string& token): m_token(token), m_cluster(cluster), m_autoposter_timer(0) { +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)); @@ -101,7 +104,7 @@ 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 @@ -142,14 +145,14 @@ void client::post_server_count(const topgg::post_server_count_completion_event& #ifdef DPP_CORO dpp::async client::co_post_server_count() { - return dpp::async{ [this] (C&& cc) { return post_server_count(std::forward(cc)); }}; + return dpp::async{[this](C&& cc) { return post_server_count(std::forward(cc)); }}; } #endif void client::get_server_count(const topgg::get_server_count_completion_event& callback) { basic_request>("/bots/" + m_id + "/stats", callback, [](const auto& j) { std::optional server_count{}; - + try { *server_count = j["server_count"].template get(); } catch (const std::exception&) {} @@ -160,7 +163,7 @@ void client::get_server_count(const topgg::get_server_count_completion_event& ca #ifdef DPP_CORO topgg::async_result> client::co_get_server_count() { - return topgg::async_result>{ [this] (C&& cc) { return get_server_count(std::forward(cc)); }}; + return topgg::async_result>{[this](C&& cc) { return get_server_count(std::forward(cc)); }}; } #endif @@ -186,7 +189,7 @@ void client::get_voters(const topgg::get_voters_completion_event& callback) { #ifdef DPP_CORO 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)); }}; + return topgg::async_result>{[this, page](C&& cc) { return get_voters(page, std::forward(cc)); }}; } #endif @@ -198,7 +201,7 @@ void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_comp #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 @@ -210,20 +213,21 @@ void client::is_weekend(const topgg::is_weekend_completion_event& 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 topgg::autopost_completion_event& callback, time_t delay) { - if (delay < TOPGG_AUTOPOSTER_MIN_DELAY) { - delay = TOPGG_AUTOPOSTER_MIN_DELAY; +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 server_count{get_server_count()}; @@ -236,20 +240,22 @@ void client::start_autoposter(const topgg::autopost_completion_event& callback, } }); } - }, delay); + }, interval); + // clang-format on } } -void client::start_autoposter(const time_t delay) { - start_autoposter([](TOPGG_UNUSED const auto&) {}, delay); +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 delay) { +void client::start_autoposter(topgg::autoposter_source* source, const topgg::autopost_completion_event& callback, time_t interval) { if (!m_autoposter_timer) { - if (delay < TOPGG_AUTOPOSTER_MIN_DELAY) { - delay = TOPGG_AUTOPOSTER_MIN_DELAY; + 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)}; @@ -262,14 +268,13 @@ void client::start_autoposter(topgg::autoposter_source* source, const topgg::aut } }); } - }, delay, [source](TOPGG_UNUSED dpp::timer) { - delete source; - }); + }, interval, [source](TOPGG_UNUSED dpp::timer) { delete source; }); + // clang-format on } } -void client::start_autoposter(topgg::autoposter_source* source, time_t delay) { - start_autoposter(source, [](TOPGG_UNUSED const auto&) {}, delay); +void client::start_autoposter(topgg::autoposter_source* source, time_t interval) { + start_autoposter(source, [](TOPGG_UNUSED const auto&) {}, interval); } void client::stop_autoposter() noexcept { diff --git a/src/models.cpp b/src/models.cpp index 56c8f0b..b62403f 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -78,7 +78,8 @@ static time_t timestamp_from_id(const dpp::snowflake& id) { return static_cast(((id >> 22) / 1000) + 1420070400); } -bot::bot(const dpp::json& j): url("https://top.gg/bot/") { +bot::bot(const dpp::json& j) + : url("https://top.gg/bot/") { id = SNOWFLAKE_FROM_JSON(j, clientid); topgg_id = SNOWFLAKE_FROM_JSON(j, id); @@ -121,7 +122,7 @@ bot::bot(const dpp::json& j): url("https://top.gg/bot/") { try { url.append(j["vanity"].template get()); } catch (TOPGG_UNUSED const std::exception&) { - url.append(std::to_string(id)); + url.append(std::to_string(topgg_id)); } const auto reviews{j["reviews"]}; @@ -202,7 +203,7 @@ void bot_query::send(const topgg::get_bots_completion_event& callback) { m_client->basic_request>(path, callback, [](const auto& j) { std::vector bots{}; - + bots.reserve(j["count"].template get()); for (const auto& bot: j["results"].template get>()) { @@ -215,7 +216,7 @@ void bot_query::send(const topgg::get_bots_completion_event& callback) { #ifdef DPP_CORO dpp::async> bot_query::co_send() { - return dpp::async>{ [this] (C&& cc) { return send(std::forward(cc)); }}; + return dpp::async>{[this](C&& cc) { return send(std::forward(cc)); }}; } #endif diff --git a/test.cpp b/test.cpp index 3131f8a..a6592bd 100644 --- a/test.cpp +++ b/test.cpp @@ -49,7 +49,7 @@ int main() { dpp::cluster bot{discord_token}; topgg::client topgg_client{bot, topgg_token}; - std::cout << "starting bot "; + std::cout << "Starting bot... "; bot.start(dpp::start_type::st_return); diff --git a/test_autoposter.cpp b/test_autoposter.cpp index a496acf..e28dc5b 100644 --- a/test_autoposter.cpp +++ b/test_autoposter.cpp @@ -27,7 +27,7 @@ int main() { dpp::cluster bot{discord_token}; topgg::client topgg_client{bot, topgg_token}; - std::cout << "starting bot "; + std::cout << "Starting bot... "; bot.start(dpp::start_type::st_return); @@ -35,7 +35,7 @@ int main() { topgg_client.start_autoposter([](const auto& result) { if (result) { - std::cout << "Successfully posted " << *result << " servers to the Top.gg API!" << std::endl; + std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; if (g_counter++ == 3) { g_sem.release(); diff --git a/topgg.rc b/topgg.rc index 97f97ba..eafeae7 100644 --- a/topgg.rc +++ b/topgg.rc @@ -16,7 +16,7 @@ BEGIN BLOCK "040904B0" BEGIN VALUE "CompanyName", "Top.gg" - VALUE "FileDescription", "The official C++ wrapper for the Top.gg API." + 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" From f21e804271adb469ef7a36610b403947be276267 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 6 Mar 2025 14:42:39 +0700 Subject: [PATCH 29/41] doc: fix not_found description --- include/topgg/client.h | 7 ++----- include/topgg/models.h | 1 - 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 7967d3f..613b928 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -193,7 +193,7 @@ namespace topgg { * @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 Such query does not exist. + * @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 @@ -315,7 +315,6 @@ namespace topgg { * * @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 co_await to retrieve an optional size_t if successful @@ -417,7 +416,6 @@ namespace topgg { * @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::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 co_await to retrieve a vector of topgg::voter if successful @@ -485,7 +483,7 @@ namespace topgg { * @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 Such query does not exist. + * @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 @@ -549,7 +547,6 @@ namespace topgg { * * @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 co_await to retrieve a bool if successful diff --git a/include/topgg/models.h b/include/topgg/models.h index 7ab84dd..d3ad9ed 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -457,7 +457,6 @@ namespace topgg { * * @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 co_await to retrieve a vector of topgg::bot if successful From 88e5a092b34dda223b720824d74939012ffe43a7 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 6 Mar 2025 17:43:25 +0700 Subject: [PATCH 30/41] chore: update date --- include/topgg/client.h | 2 +- include/topgg/models.h | 2 +- include/topgg/result.h | 2 +- include/topgg/topgg.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 613b928..c11c24e 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @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-02-25 + * @date 2025-03-06 * @version 3.0.0 */ diff --git a/include/topgg/models.h b/include/topgg/models.h index d3ad9ed..eb29bb1 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @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-02-25 + * @date 2025-03-06 * @version 3.0.0 */ diff --git a/include/topgg/result.h b/include/topgg/result.h index 97bad68..59aa732 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @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-02-25 + * @date 2025-03-06 * @version 3.0.0 */ diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index 80c9f91..818762b 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,7 +4,7 @@ * @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-02-25 + * @date 2025-03-06 * @version 3.0.0 */ From ece1fd239fe6e2b1bce7635255ed6af3fd4c28a9 Mon Sep 17 00:00:00 2001 From: null8626 Date: Sat, 8 Mar 2025 06:48:14 +0700 Subject: [PATCH 31/41] refactor: remove the need to destruct API token --- include/topgg/client.h | 3 +- include/topgg/models.h | 6 +-- include/topgg/result.h | 2 +- include/topgg/topgg.h | 2 +- src/client.cpp | 95 ++---------------------------------------- src/models.cpp | 4 +- 6 files changed, 12 insertions(+), 100 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index c11c24e..d04037a 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @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-03-06 + * @date 2025-03-08 * @version 3.0.0 */ @@ -83,7 +83,6 @@ namespace topgg { 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; diff --git a/include/topgg/models.h b/include/topgg/models.h index eb29bb1..d402751 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @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-03-06 + * @date 2025-03-08 * @version 3.0.0 */ @@ -79,7 +79,7 @@ namespace topgg { * * @since 3.0.0 */ - std::string name; + std::string username; /** * @brief This voter's creation date. @@ -131,7 +131,7 @@ namespace topgg { * * @since 3.0.0 */ - std::string name; + std::string username; /** * @brief This bot's creation date. diff --git a/include/topgg/result.h b/include/topgg/result.h index 59aa732..6b180a9 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @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-03-06 + * @date 2025-03-08 * @version 3.0.0 */ diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index 818762b..9aec17e 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,7 +4,7 @@ * @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-03-06 + * @date 2025-03-08 * @version 3.0.0 */ diff --git a/src/client.cpp b/src/client.cpp index 19e1308..6fd615c 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -2,95 +2,8 @@ using topgg::client; -// 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 Discord Bot 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++")); @@ -128,7 +41,7 @@ void client::post_server_count_inner(const size_t server_count, dpp::http_comple j["server_count"] = server_count; - m_cluster.request(TOPGG_BASE_URL "/bots/" + m_id + "/stats", dpp::m_post, callback, j.dump(), "application/json", headers); + m_cluster.request(TOPGG_BASE_URL "/bots/stats", dpp::m_post, callback, j.dump(), "application/json", headers); } void client::post_server_count(const topgg::post_server_count_completion_event& callback) { @@ -150,7 +63,7 @@ dpp::async client::co_post_server_count() { #endif void client::get_server_count(const topgg::get_server_count_completion_event& callback) { - basic_request>("/bots/" + m_id + "/stats", callback, [](const auto& j) { + basic_request>("/bots/stats", callback, [](const auto& j) { std::optional server_count{}; try { @@ -172,7 +85,7 @@ void client::get_voters(size_t page, const topgg::get_voters_completion_event& c page = 1; } - basic_request>("/bots/" + m_id + "/votes?page=" + std::to_string(page), callback, [](const auto& j) { + basic_request>("/bots/votes?page=" + std::to_string(page), callback, [](const auto& j) { std::vector voters{}; for (const auto& part: j) { @@ -194,7 +107,7 @@ topgg::async_result> client::co_get_voters(size_t page #endif void client::has_voted(const dpp::snowflake user_id, const topgg::has_voted_completion_event& callback) { - basic_request("/bots/" + m_id + "/check?userId=" + std::to_string(user_id), callback, [](const auto& j) { + basic_request("/bots/check?userId=" + std::to_string(user_id), callback, [](const auto& j) { return j["voted"].template get() != 0; }); } diff --git a/src/models.cpp b/src/models.cpp index b62403f..2a02250 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -83,7 +83,7 @@ bot::bot(const dpp::json& j) id = SNOWFLAKE_FROM_JSON(j, clientid); topgg_id = SNOWFLAKE_FROM_JSON(j, id); - DESERIALIZE_ALIAS(j, username, name, std::string); + DESERIALIZE(j, username, std::string); DESERIALIZE(j, avatar, std::string); created_at = timestamp_from_id(id); @@ -223,7 +223,7 @@ dpp::async> bot_query::co_send() { voter::voter(const dpp::json& j) { id = SNOWFLAKE_FROM_JSON(j, id); - DESERIALIZE_ALIAS(j, username, name, std::string); + DESERIALIZE(j, username, std::string); DESERIALIZE(j, avatar, std::string); created_at = timestamp_from_id(id); From b548a3f43dfe067d4a53964c0671e99e9d40b1c6 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 5 May 2025 22:23:28 +0700 Subject: [PATCH 32/41] feat: remove url and bannerUrl --- include/topgg/models.h | 13 +++---------- src/models.cpp | 14 +++----------- 2 files changed, 6 insertions(+), 21 deletions(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index d402751..1c3bf11 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -190,13 +190,6 @@ namespace topgg { */ std::vector owners; - /** - * @brief This bot's banner URL. - * - * @since 2.0.0 - */ - std::optional banner; - /** * @brief This bot's submission date. * @@ -233,11 +226,11 @@ namespace topgg { std::optional invite; /** - * @brief This bot's Top.gg page URL. + * @brief This bot's Top.gg vanity code. * - * @since 2.0.0 + * @since 3.0.0 */ - std::string url; + std::optional vanity; /** * @brief This bot's posted server count. diff --git a/src/models.cpp b/src/models.cpp index 2a02250..294a5f2 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -78,8 +78,7 @@ static time_t timestamp_from_id(const dpp::snowflake& id) { return static_cast(((id >> 22) / 1000) + 1420070400); } -bot::bot(const dpp::json& j) - : url("https://top.gg/bot/") { +bot::bot(const dpp::json& j) { id = SNOWFLAKE_FROM_JSON(j, clientid); topgg_id = SNOWFLAKE_FROM_JSON(j, id); @@ -105,8 +104,6 @@ bot::bot(const dpp::json& j) } }); - DESERIALIZE_OPTIONAL_STRING_ALIAS(j, bannerUrl, banner); - const auto j_submitted_at{j["date"].template get()}; tm submitted_at_tm; @@ -116,15 +113,10 @@ bot::bot(const dpp::json& j) DESERIALIZE_ALIAS(j, points, votes, size_t); DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); DESERIALIZE_OPTIONAL(j, invite, std::string); + DESERIALIZE_OPTIONAL(j, vanity, std::string); DESERIALIZE_OPTIONAL(j, support, std::string); DESERIALIZE_OPTIONAL(j, server_count, size_t); - - try { - url.append(j["vanity"].template get()); - } catch (TOPGG_UNUSED const std::exception&) { - url.append(std::to_string(topgg_id)); - } - + const auto reviews{j["reviews"]}; DESERIALIZE_ALIAS(reviews, averageScore, review_score, double); From 3d87f13beade6db09c145919953d72a079904e9e Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 23 May 2025 13:25:42 +0700 Subject: [PATCH 33/41] fix: GET /bots/votes not working --- include/topgg/client.h | 1 + src/client.cpp | 89 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 1 deletion(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index d04037a..42cc676 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -83,6 +83,7 @@ namespace topgg { 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; diff --git a/src/client.cpp b/src/client.cpp index 6fd615c..c91d602 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -2,8 +2,95 @@ using topgg::client; +// 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 Discord Bot 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++")); @@ -85,7 +172,7 @@ void client::get_voters(size_t page, const topgg::get_voters_completion_event& c page = 1; } - basic_request>("/bots/votes?page=" + std::to_string(page), callback, [](const auto& j) { + basic_request>("/bots/" + m_id + "/votes?page=" + std::to_string(page), callback, [](const auto& j) { std::vector voters{}; for (const auto& part: j) { From c94d19cb44ea1537a4b5643010dc63560a1ebb6e Mon Sep 17 00:00:00 2001 From: null <60427892+null8626@users.noreply.github.com> Date: Fri, 23 May 2025 14:36:26 +0700 Subject: [PATCH 34/41] doc: update error message --- src/client.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/client.cpp b/src/client.cpp index c91d602..1c06d73 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -84,7 +84,7 @@ static std::string id_from_bot_token(std::string bot_token) { } } - throw std::invalid_argument{"Got a malformed Discord Bot token."}; + throw std::invalid_argument{"Got a malformed API token."}; } client::client(dpp::cluster& cluster, const std::string& token) @@ -286,4 +286,4 @@ void client::stop_autoposter() noexcept { client::~client() { stop_autoposter(); -} \ No newline at end of file +} From fc297c85ed9fb43642edd2d5cab0d8bc7c9995f0 Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 16 Jun 2025 17:51:19 +0700 Subject: [PATCH 35/41] feat: fully adapt to v0 --- include/topgg/client.h | 2 -- include/topgg/models.h | 61 ------------------------------------------ src/models.cpp | 27 ------------------- test.cpp | 1 - 4 files changed, 91 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 42cc676..25d3bd5 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -219,7 +219,6 @@ namespace topgg { * .get_bots() * .limit(250) * .skip(50) - * .name("shiro") * .sort_by_monthly_votes() * .send([](const auto& result) { * try { @@ -245,7 +244,6 @@ namespace topgg { * .get_bots() * .limit(250) * .skip(50) - * .name("shiro") * .sort_by_monthly_votes() * .send(); * diff --git a/include/topgg/models.h b/include/topgg/models.h index 1c3bf11..5773d97 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -39,12 +39,6 @@ return *this; \ } -#define TOPGG_BOT_QUERY_SEARCH(type, lib_name, api_name, ...) \ - inline bot_query& lib_name(const type lib_name) { \ - add_search(#api_name, lib_name, __VA_ARGS__); \ - return *this; \ - } - namespace topgg { class bot_query; class client; @@ -275,15 +269,12 @@ namespace topgg { class TOPGG_EXPORT bot_query { client* m_client; std::unordered_map m_query; - std::unordered_map m_search; 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); - void add_search(const char* key, const std::string& value); - void add_search(const char* key, const size_t value); public: bot_query() = delete; @@ -335,56 +326,6 @@ namespace topgg { */ TOPGG_BOT_QUERY_QUERY(uint16_t, skip, offset, 499); - /** - * @brief Queries only bots that has this username. - * - * @param username The bot's username. - * @return bot_query The current modified object. - * @see topgg::client::get_bots - * @since 2.0.1 - */ - TOPGG_BOT_QUERY_SEARCH(std::string&, name, username); - - /** - * @brief Queries only bots that has this prefix. - * - * @param prefix The bot's prefix. - * @return bot_query The current modified object. - * @see topgg::client::get_bots - * @since 2.0.1 - */ - TOPGG_BOT_QUERY_SEARCH(std::string&, prefix, prefix); - - /** - * @brief Queries only bots that has this vote count. - * - * @param votes The bot's vote count. - * @return bot_query The current modified object. - * @see topgg::client::get_bots - * @since 2.0.1 - */ - TOPGG_BOT_QUERY_SEARCH(size_t, votes, points); - - /** - * @brief Queries only bots that has this monthly vote count. - * - * @param monthly_votes The bot's monthly vote count. - * @return bot_query The current modified object. - * @see topgg::client::get_bots - * @since 2.0.1 - */ - TOPGG_BOT_QUERY_SEARCH(size_t, monthly_votes, monthlyPoints); - - /** - * @brief Queries only bots that has this Top.gg vanity URL. - * - * @param vanity The bot's Top.gg vanity URL. - * @return bot_query The current modified object. - * @see topgg::client::get_bots - * @since 2.0.1 - */ - TOPGG_BOT_QUERY_SEARCH(std::string&, vanity, vanity); - /** * @brief Sends the query to the API. * @@ -398,7 +339,6 @@ namespace topgg { * .get_bots() * .limit(250) * .skip(50) - * .name("shiro") * .sort_by_monthly_votes() * .send([](const auto& result) { * try { @@ -436,7 +376,6 @@ namespace topgg { * .get_bots() * .limit(250) * .skip(50) - * .name("shiro") * .sort_by_monthly_votes() * .send(); * diff --git a/src/models.cpp b/src/models.cpp index 294a5f2..518d046 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -148,14 +148,6 @@ void bot_query::add_query(const char* key, const uint16_t value, const uint16_t m_query.insert_or_assign(key, std::to_string(std::min(value, max))); } -void bot_query::add_search(const char* key, const std::string& value) { - m_search.insert_or_assign(key, querystring(value)); -} - -void bot_query::add_search(const char* key, const size_t value) { - m_search.insert_or_assign(key, std::to_string(value)); -} - void bot_query::send(const topgg::get_bots_completion_event& callback) { std::string path{"/bots?"}; @@ -165,25 +157,6 @@ void bot_query::send(const topgg::get_bots_completion_event& callback) { path.push_back('&'); } - std::string search{}; - - for (const auto& search_query: m_search) { - search.append("%20"); - search.append(search_query.first); - search.append("%3A%20"); - search.append(search_query.second); - } - - if (!search.empty()) { - const auto search_raw{search.c_str() + 3}; - - if (*search_raw != 0) { - path.append("search="); - path.append(search_raw); - path.push_back('&'); - } - } - for (const auto& additional_query: m_query) { path.append(additional_query.first); path.push_back('='); diff --git a/test.cpp b/test.cpp index a6592bd..e4db04b 100644 --- a/test.cpp +++ b/test.cpp @@ -64,7 +64,6 @@ int main() { .get_bots() .limit(250) .skip(50) - .name("shiro") .sort_by_monthly_votes() .send(TEST_RESULT_CALLBACK()); From fdd65df9d6500e4c8d43409051f4c1d05ad9dd9e Mon Sep 17 00:00:00 2001 From: null8626 Date: Mon, 16 Jun 2025 17:53:15 +0700 Subject: [PATCH 36/41] meta: update dates --- include/topgg/client.h | 2 +- include/topgg/models.h | 2 +- include/topgg/result.h | 2 +- include/topgg/topgg.h | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index 25d3bd5..e3ad898 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @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-03-08 + * @date 2025-06-16 * @version 3.0.0 */ diff --git a/include/topgg/models.h b/include/topgg/models.h index 5773d97..b0f4668 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @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-03-08 + * @date 2025-06-16 * @version 3.0.0 */ diff --git a/include/topgg/result.h b/include/topgg/result.h index 6b180a9..30b8b2f 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @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-03-08 + * @date 2025-06-16 * @version 3.0.0 */ diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index 9aec17e..c31c89b 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,7 +4,7 @@ * @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-03-08 + * @date 2025-06-16 * @version 3.0.0 */ From 4d336a69324f3a54dc6917fe28755410b7de2c93 Mon Sep 17 00:00:00 2001 From: null8626 Date: Tue, 17 Jun 2025 15:15:52 +0700 Subject: [PATCH 37/41] feat: widgets --- include/topgg/client.h | 2 +- include/topgg/models.h | 20 +++++++++++++++++++- include/topgg/result.h | 2 +- include/topgg/topgg.h | 4 ++-- src/client.cpp | 2 +- src/models.cpp | 4 ++++ 6 files changed, 28 insertions(+), 6 deletions(-) diff --git a/include/topgg/client.h b/include/topgg/client.h index e3ad898..5dcfb29 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @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-06-16 + * @date 2025-06-17 * @version 3.0.0 */ diff --git a/include/topgg/models.h b/include/topgg/models.h index b0f4668..b09bb0d 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @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-06-16 + * @date 2025-06-17 * @version 3.0.0 */ @@ -413,6 +413,24 @@ namespace topgg { 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(264811613708746752)}; + * + * std::cout << widget_url << std::endl; + * ``` + * + * @param id The ID. + * @since 3.0.0 + */ + std::string large(const dpp::snowflake id); + }; // namespace widget }; // namespace topgg #undef TOPGG_BOT_QUERY_SEARCH diff --git a/include/topgg/result.h b/include/topgg/result.h index 30b8b2f..636d005 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @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-06-16 + * @date 2025-06-17 * @version 3.0.0 */ diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index c31c89b..d17dcc0 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,7 +4,7 @@ * @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-06-16 + * @date 2025-06-17 * @version 3.0.0 */ @@ -54,7 +54,7 @@ #pragma clang diagnostic pop #endif -#define TOPGG_BASE_URL "https://top.gg/api" +#define TOPGG_BASE_URL "https://top.gg/api/v1" #include #include diff --git a/src/client.cpp b/src/client.cpp index 1c06d73..a32b91e 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -286,4 +286,4 @@ void client::stop_autoposter() noexcept { client::~client() { stop_autoposter(); -} +} \ No newline at end of file diff --git a/src/models.cpp b/src/models.cpp index 518d046..f7df5ed 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -193,3 +193,7 @@ voter::voter(const dpp::json& j) { created_at = timestamp_from_id(id); } + +std::string topgg::widget::large(const dpp::snowflake id) { + return TOPGG_BASE_URL "/widgets/large/" + std::to_string(id); +} \ No newline at end of file From d777380c569a7f5e4d41f2192d7933f9526b51a2 Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 18 Jun 2025 20:39:33 +0700 Subject: [PATCH 38/41] feat: add small widgets --- include/topgg/models.h | 61 +++++++++++++++++++++++++++++++++++++++--- src/models.cpp | 16 +++++++++-- 2 files changed, 72 insertions(+), 5 deletions(-) diff --git a/include/topgg/models.h b/include/topgg/models.h index b09bb0d..bb126d7 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -27,6 +27,9 @@ #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; \ @@ -421,18 +424,70 @@ namespace topgg { * Example: * * ```cpp - * const auto widget_url{topgg::widget::large(264811613708746752)}; + * 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 large(const dpp::snowflake id); + 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 +#undef TOPGG_BOT_QUERY_SORT \ No newline at end of file diff --git a/src/models.cpp b/src/models.cpp index f7df5ed..5e13ef8 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -194,6 +194,18 @@ voter::voter(const dpp::json& j) { created_at = timestamp_from_id(id); } -std::string topgg::widget::large(const dpp::snowflake id) { - return TOPGG_BASE_URL "/widgets/large/" + std::to_string(id); +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); +} + +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); +} + +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 From 49f09617cf8b3739ad9cdeaf4fe5f50bd4bb3949 Mon Sep 17 00:00:00 2001 From: null8626 Date: Fri, 20 Jun 2025 15:30:26 +0700 Subject: [PATCH 39/41] doc: documentation overhaul --- README.md | 365 ++++++++++++++++++++++++++++++++++------- include/topgg/client.h | 82 ++++----- include/topgg/models.h | 8 +- test.cpp | 16 +- test_autoposter.cpp | 4 +- 5 files changed, 359 insertions(+), 116 deletions(-) diff --git a/README.md b/README.md index a9fabd4..1621a3a 100644 --- a/README.md +++ b/README.md @@ -1,113 +1,237 @@ -# Top.gg SDK for C++ +# Top.gg C++ SDK -A simple API wrapper for [Top.gg](https://top.gg) written in C++. +The community-maintained C++17 library for Top.gg. ## Building from source +First, clone the git repository like so: + +```sh +$ git clone https://github.com/Top-gg-Community/cpp-sdk --depth 1 +``` + **NOTE:** To enable C++20 coroutine methods, add `-DENABLE_CORO=ON`! ### 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 +$ dpkg -i dpp.deb +``` -# build topgg -cmake -B build . -cmake --build build --config Release +Build `topgg`: + +```sh +$ cmake -B build . +$ cmake --build build --config Release ``` ### 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 +$ yum install wget +$ wget -O dpp.rpm https://dl.dpp.dev/latest/linux-x64/rpm +$ 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 +Install D++: + ```sh -# install D++ -brew install libdpp -brew link libdpp +$ brew install libdpp +$ brew link libdpp +``` -# build topgg -cmake -B build . -cmake --build build --config Release +Build `topgg`: + +```sh +$ cmake -B build . +$ cmake --build build --config Release ``` ### Windows ```bat -cmake -B build . -cmake --build build --config Release +> cmake -B build . +> cmake --build build --config Release ``` -## Examples - -### Fetching a bot from its Discord ID +## Setting up ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +#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; + } + + dpp::cluster bot{discord_token}; + topgg::client client{bot, topgg_token}; + + return 0; +} +``` + +## Usage + +### Getting a bot -// Using C++17 callbacks -topgg_client.get_bot(264811613708746752, [](const auto& result) { +#### 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::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::cerr << "error: " << exc.what() << std::endl; } ``` -### Posting your bot's server count +### Getting several bots + +#### With C++17 callbacks ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_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; + } + }); +``` -// Using C++17 callbacks -topgg_client.post_server_count([](const auto success) { - if (success) { - std::cout << "Stats posted!" << std::endl; +#### With C++20 coroutines + +```cpp +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 + +#### With C++17 callbacks + +```cpp +// First page +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; } }); -// Using C++20 coroutines -const auto success = co_await topgg_client.co_post_server_count(); +// Subsequent pages +client.get_voters(2, [](const auto& result) { + try { + auto voters = result.get(); -if (success) { - std::cout << "Stats posted!" << std::endl; -} + for (auto& voter: voters) { + std::cout << voter.username << std::endl; + } + } catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; + } +}); ``` -### Checking if a user has voted your bot +#### With C++20 coroutines ```cpp -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +// First page +try { + const auto voters = co_await client.co_get_voters(); -// Using C++17 callbacks -topgg_client.has_voted(661200758510977084, [](const auto& result) { + 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; @@ -116,10 +240,13 @@ topgg_client.has_voted(661200758510977084, [](const auto& result) { std::cerr << "error: " << exc.what() << std::endl; } }); +``` -// Using C++20 coroutines +#### With C++20 coroutines + +```cpp 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; @@ -129,16 +256,75 @@ try { } ``` -### Default autoposting +### Getting your bot's server count + +#### With C++17 callbacks + +```cpp +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; + } +}); +``` + +#### 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 -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +client.post_server_count([](const auto success) { + if (success) { + std::cout << "Stats posted!" << std::endl; + } +}); +``` -topgg_client.start_autoposter(); +#### With C++20 coroutines + +```cpp +const auto success = co_await client.co_post_server_count(); + +if (success) { + std::cout << "Stats posted!" << std::endl; +} +``` + +### Automatically posting your bot's server count every few minutes + +#### Without a callback + +```cpp +client.start_autoposter(); +``` + +#### With a callback + +```cpp +client.start_autoposter([](const auto& result) { + if (result) { + std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; + } +}); ``` -### Customized autoposting +#### From a custom source ```cpp class my_autoposter_source: private topgg::autoposter_source { @@ -148,8 +334,65 @@ public: } }; -dpp::cluster bot{"your bot token"}; -topgg::client topgg_client{bot, "your top.gg token"}; +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 << "The weekend multiplier is active" << std::endl; + } + } catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; + } +}); +``` + +#### With C++20 coroutines + +```cpp +try { + const auto is_weekend = co_await client.co_is_weekend(); + + if (is_weekend) { + std::cout << "The weekend multiplier is active" << std::endl; + } +} catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; +} +``` + +### Generating widget URLs + +#### Large + +```cpp +const auto widget_url{topgg::widget::large(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; +``` -topgg_client.start_autoposter(reinterpret_cast(new my_autoposter_source)); +#### Votes + +```cpp +const auto widget_url{topgg::widget::votes(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; +``` + +#### Owner + +```cpp +const auto widget_url{topgg::widget::owner(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; +``` + +#### Social + +```cpp +const auto widget_url{topgg::widget::social(TOPGG_WIDGET_DISCORD_BOT, 264811613708746752)}; ``` \ No newline at end of file diff --git a/include/topgg/client.h b/include/topgg/client.h index 5dcfb29..ed7085f 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -148,9 +148,9 @@ namespace topgg { * * ```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(); * @@ -179,10 +179,10 @@ namespace topgg { * * ```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) { @@ -213,9 +213,9 @@ namespace topgg { * * ```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 + * client * .get_bots() * .limit(250) * .skip(50) @@ -237,10 +237,10 @@ namespace topgg { * * ```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 bots = co_await topgg_client + * const auto bots = co_await client * .get_bots() * .limit(250) * .skip(50) @@ -270,9 +270,9 @@ namespace topgg { * * ```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_server_count([](const auto& result) { + * client.get_server_count([](const auto& result) { * try { * auto server_count = result.get(); * @@ -300,10 +300,10 @@ namespace topgg { * * ```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 server_count = co_await topgg_client.co_get_server_count(); + * 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) { @@ -332,9 +332,9 @@ namespace topgg { * * ```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(1, [](const auto& result) { + * client.get_voters(1, [](const auto& result) { * try { * auto voters = result.get(); * @@ -365,9 +365,9 @@ namespace topgg { * * ```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([](const auto& result) { * try { * auto voters = result.get(); * @@ -398,10 +398,10 @@ namespace topgg { * * ```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; @@ -434,9 +434,9 @@ namespace topgg { * * ```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; @@ -465,10 +465,10 @@ namespace topgg { * * ```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; @@ -501,9 +501,9 @@ namespace topgg { * * ```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; @@ -530,10 +530,10 @@ namespace topgg { * * ```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; @@ -563,9 +563,9 @@ namespace topgg { * * ```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_server_count([](const auto success) { + * client.post_server_count([](const auto success) { * if (success) { * std::cout << "Stats posted!" << std::endl; * } @@ -589,9 +589,9 @@ namespace topgg { * * ```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_server_count(); + * const auto success = co_await client.co_post_server_count(); * * if (success) { * std::cout << "Stats posted!" << std::endl; @@ -614,9 +614,9 @@ namespace topgg { * * ```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([](const auto& result) { + * client.start_autoposter([](const auto& result) { * if (result) { * std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; * } @@ -640,9 +640,9 @@ namespace topgg { * * ```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(); * ``` * * @param interval The interval between posting in seconds. Defaults to 15 minutes. @@ -667,9 +667,9 @@ namespace topgg { * }; * * 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.start_autoposter(reinterpret_cast(new my_autoposter_source), [](const auto& result) { + * 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; * } @@ -702,9 +702,9 @@ namespace topgg { * }; * * 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.start_autoposter(reinterpret_cast(new my_autoposter_source)); + * client.start_autoposter(reinterpret_cast(new my_autoposter_source)); * ``` * * @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. @@ -724,13 +724,13 @@ namespace topgg { * * ```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. diff --git a/include/topgg/models.h b/include/topgg/models.h index bb126d7..9cb99ee 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -336,9 +336,9 @@ namespace topgg { * * ```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 + * client * .get_bots() * .limit(250) * .skip(50) @@ -372,10 +372,10 @@ namespace topgg { * * ```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 bots = co_await topgg_client + * const auto bots = co_await client * .get_bots() * .limit(250) * .skip(50) diff --git a/test.cpp b/test.cpp index e4db04b..b95d6cd 100644 --- a/test.cpp +++ b/test.cpp @@ -47,7 +47,7 @@ int main() { } dpp::cluster bot{discord_token}; - topgg::client topgg_client{bot, topgg_token}; + topgg::client client{bot, topgg_token}; std::cout << "Starting bot... "; @@ -55,12 +55,12 @@ int main() { std::cout << "ok\nget_bot "; - topgg_client.get_bot(264811613708746752, TEST_RESULT_CALLBACK()); + client.get_bot(264811613708746752, TEST_RESULT_CALLBACK()); ACQUIRE_TEST_THREAD(); std::cout << "get_bots "; - topgg_client + client .get_bots() .limit(250) .skip(50) @@ -70,12 +70,12 @@ int main() { ACQUIRE_TEST_THREAD(); std::cout << "has_voted "; - topgg_client.has_voted(661200758510977084, TEST_RESULT_CALLBACK()); + client.has_voted(661200758510977084, TEST_RESULT_CALLBACK()); ACQUIRE_TEST_THREAD(); std::cout << "post_server_count "; - topgg_client.post_server_count([](const auto success) { + client.post_server_count([](const auto success) { if (success) { std::cout << "ok" << std::endl; } else { @@ -89,17 +89,17 @@ int main() { ACQUIRE_TEST_THREAD(); std::cout << "get_server_count "; - topgg_client.get_server_count(TEST_RESULT_CALLBACK()); + client.get_server_count(TEST_RESULT_CALLBACK()); ACQUIRE_TEST_THREAD(); std::cout << "get_voters "; - topgg_client.get_voters(TEST_RESULT_CALLBACK()); + client.get_voters(TEST_RESULT_CALLBACK()); ACQUIRE_TEST_THREAD(); std::cout << "is_weekend "; - topgg_client.is_weekend(TEST_RESULT_CALLBACK()); + client.is_weekend(TEST_RESULT_CALLBACK()); ACQUIRE_TEST_THREAD(); diff --git a/test_autoposter.cpp b/test_autoposter.cpp index e28dc5b..2bd7164 100644 --- a/test_autoposter.cpp +++ b/test_autoposter.cpp @@ -25,7 +25,7 @@ int main() { } dpp::cluster bot{discord_token}; - topgg::client topgg_client{bot, topgg_token}; + topgg::client client{bot, topgg_token}; std::cout << "Starting bot... "; @@ -33,7 +33,7 @@ int main() { std::cout << "ok\n"; - topgg_client.start_autoposter([](const auto& result) { + client.start_autoposter([](const auto& result) { if (result) { std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; From c3345ace2ab6370ee7547d7f48287ec26644ac2c Mon Sep 17 00:00:00 2001 From: null8626 Date: Wed, 2 Jul 2025 18:15:32 +0700 Subject: [PATCH 40/41] feat: add webhooks --- .gitignore | 20 +- .gitmodules | 3 + CMakeLists.txt | 99 ++++++- README.md | 275 +++++++++++++++++- cmake/FindDPP.cmake | 10 +- conanfile.txt | 3 + deps/drogon | 1 + include/topgg/client.h | 2 +- include/topgg/export.h | 38 +++ include/topgg/models.h | 6 +- include/topgg/result.h | 2 +- include/topgg/topgg.h | 29 +- include/topgg/webhooks/cpp-httplib.h | 82 ++++++ include/topgg/webhooks/drogon.h | 84 ++++++ include/topgg/webhooks/models.h | 79 +++++ src/models.cpp | 110 +++---- src/webhooks/cpp-httplib.cpp | 35 +++ src/webhooks/drogon.cpp | 45 +++ src/webhooks/models.cpp | 69 +++++ tests/CMakeLists.txt | 78 +++++ tests/cpp-httplib-webhooks/test_vote.cpp | 61 ++++ tests/drogon-webhooks/test_vote.cpp | 77 +++++ test.cpp => tests/test_api.cpp | 4 - .../test_autoposter.cpp | 4 - 24 files changed, 1094 insertions(+), 122 deletions(-) create mode 100644 conanfile.txt create mode 160000 deps/drogon create mode 100644 include/topgg/export.h create mode 100644 include/topgg/webhooks/cpp-httplib.h create mode 100644 include/topgg/webhooks/drogon.h create mode 100644 include/topgg/webhooks/models.h create mode 100644 src/webhooks/cpp-httplib.cpp create mode 100644 src/webhooks/drogon.cpp create mode 100644 src/webhooks/models.cpp create mode 100644 tests/CMakeLists.txt create mode 100644 tests/cpp-httplib-webhooks/test_vote.cpp create mode 100644 tests/drogon-webhooks/test_vote.cpp rename test.cpp => tests/test_api.cpp (97%) rename test_autoposter.cpp => tests/test_autoposter.cpp (95%) diff --git a/.gitignore b/.gitignore index 024ba54..f5e1998 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,25 @@ **/*.deb **/*.rpm +**/*.exe +**/*.obj +*.cmake .idea + cmake-build-debug/ +CMakePresets.json .vscode/ include/dpp/ -build/ -deps/ +include/cpp-httplib/ +include/drogon/ +include/nlohmann/ +**/build/ +**/*.dll +**/*.lib docs/html/ -test.exe -test.obj \ No newline at end of file +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 6cad2e9..31ca8c4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,4 +1,4 @@ -cmake_minimum_required(VERSION 3.10) +cmake_minimum_required(VERSION 3.15) project( topgg @@ -10,24 +10,37 @@ project( set(CMAKE_VERBOSE_MAKEFILE ON) set(CMAKE_WINDOWS_EXPORT_ALL_SYMBOLS ON) set(CMAKE_BUILD_TYPE Release CACHE STRING "Build type") + option(BUILD_SHARED_LIBS "Build shared libraries" ON) -option(ENABLE_CORO "Support for C++20 coroutines" OFF) +option(ENABLE_API "Build primary API support" ON) +option(ENABLE_CPP_HTTPLIB_WEBHOOKS "Build support for webhooks via cpp-httplib" OFF) +option(ENABLE_DROGON_WEBHOOKS "Build support for webhooks via drogon" OFF) +option(ENABLE_CORO "Add support for C++20 coroutines" OFF) option(TESTING "Enable this only if you are testing the library" OFF) +if(ENABLE_API) file(GLOB TOPGG_SOURCE_FILES src/*.cpp) +endif() + +if(ENABLE_CPP_HTTPLIB_WEBHOOKS) +set(TOPGG_SOURCE_FILES ${TOPGG_SOURCE_FILES} src/webhooks/cpp-httplib.cpp src/webhooks/models.cpp) +elseif(ENABLE_DROGON_WEBHOOKS) +set(TOPGG_SOURCE_FILES ${TOPGG_SOURCE_FILES} src/webhooks/drogon.cpp src/webhooks/models.cpp) +endif() if(BUILD_SHARED_LIBS) add_library(topgg SHARED ${TOPGG_SOURCE_FILES}) if(WIN32) -target_sources(topgg PRIVATE ${CMAKE_SOURCE_DIR}/topgg.rc) +target_sources(topgg PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/topgg.rc) +target_compile_definitions(topgg PRIVATE __TOPGG_BUILDING_DLL__) endif() else() add_library(topgg STATIC ${TOPGG_SOURCE_FILES}) -endif() if(WIN32) -target_compile_definitions(topgg PRIVATE $,__TOPGG_BUILDING_DLL__,DPP_STATIC TOPGG_STATIC>) +target_compile_definitions(topgg PUBLIC DPP_STATIC TOPGG_STATIC) +endif() endif() if(ENABLE_CORO) @@ -42,24 +55,88 @@ 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) +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 PRIVATE __TOPGG_DROGON_WEBHOOKS__) + +if(WIN32) +target_compile_definitions(topgg PRIVATE _CRT_SECURE_NO_WARNINGS) +cmake_policy(SET CMP0091 NEW) +endif() +endif() if(MSVC) -target_compile_options(topgg PUBLIC $<$:/diagnostics:caret /MTd /DDEBUG /D_DEBUG> $<$:/MT /O2 /Oi /Oy /Gy /DNDEBUG>) +target_compile_options(topgg PRIVATE /nologo $<$:/diagnostics:caret /MDd /DDEBUG /D_DEBUG> $<$:/MD /O2 /Oi /Oy /Gy /DNDEBUG>) else() -target_compile_options(topgg PUBLIC $<$:-O3> -Wall -Wextra -Wpedantic -Wformat=2 -Wnull-dereference -Wuninitialized -Wdeprecated) +target_compile_options(topgg PRIVATE $<$:-O3> -Wall -Wextra -Wpedantic -Wformat=2 -Wnull-dereference -Wuninitialized -Wdeprecated) endif() target_include_directories(topgg PUBLIC - ${CMAKE_SOURCE_DIR}/include + ${CMAKE_CURRENT_SOURCE_DIR}/include ${DPP_INCLUDE_DIR} + ${JSONCPP_INCLUDE_DIRS} + ${ZLIB_INCLUDE_DIR} + ${TRANTOR_INCLUDE_DIR} + ${DROGON_INCLUDE_DIR} ) -target_link_libraries(topgg ${DPP_LIBRARIES}) \ No newline at end of file +target_link_libraries(topgg PUBLIC ${DPP_LIBRARIES} ${JSONCPP_LIBRARIES} ${DROGON_LIBRARY}) \ No newline at end of file diff --git a/README.md b/README.md index 1621a3a..95ef02c 100644 --- a/README.md +++ b/README.md @@ -8,17 +8,30 @@ First, clone the git repository like so: ```sh $ git clone https://github.com/Top-gg-Community/cpp-sdk --depth 1 +$ cd cpp-sdk +$ git submodule update --init --recursive ``` -**NOTE:** To enable C++20 coroutine methods, add `-DENABLE_CORO=ON`! +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: -### Linux (Debian-like) +| 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 $ wget -O dpp.deb https://dl.dpp.dev/latest -$ dpkg -i dpp.deb +$ sudo dpkg -i dpp.deb ``` Build `topgg`: @@ -28,14 +41,14 @@ $ cmake -B build . $ cmake --build build --config Release ``` -### Linux (CentOS-like) +#### Linux (CentOS-like) Install D++: ```sh -$ yum install wget +$ sudo yum install wget $ wget -O dpp.rpm https://dl.dpp.dev/latest/linux-x64/rpm -$ yum localinstall dpp.rpm +$ sudo yum localinstall dpp.rpm ``` Build `topgg`: @@ -45,7 +58,7 @@ $ cmake -B build . $ cmake --build build --config Release ``` -### macOS +#### macOS Install D++: @@ -61,16 +74,158 @@ $ cmake -B build . $ cmake --build build --config Release ``` -### Windows +#### Windows + +Install D++ and build `topgg`: ```bat > cmake -B build . > cmake --build build --config Release ``` +### Webhooks only + +#### cpp-httplib + +Install `cpp-httplib` and build `topgg`: + +```bat +> 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 +``` + +Install Drogon and build `topgg`: + +```sh +$ cmake -DENABLE_API=OFF -DENABLE_DROGON_WEBHOOKS=ON -B build . +$ cmake --build build --config Release +``` + +##### 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 +// If you compiled this library statically, uncomment the following: +// #ifndef TOPGG_STATIC +// #define TOPGG_STATIC +// #endif + #include #include @@ -319,7 +474,7 @@ client.start_autoposter(); ```cpp client.start_autoposter([](const auto& result) { if (result) { - std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; + std::cout << "Successfully posted " << *result << " servers to Top.gg!" << std::endl; } }); ``` @@ -336,7 +491,7 @@ public: 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; + std::cout << "Successfully posted " << *result << " servers to Top.gg!" << std::endl; } }); ``` @@ -395,4 +550,104 @@ const auto widget_url{topgg::widget::owner(TOPGG_WIDGET_DISCORD_BOT, 26481161370 ```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: +// #ifndef TOPGG_STATIC +// #define TOPGG_STATIC +// #endif + +#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: +// #ifndef TOPGG_STATIC +// #define TOPGG_STATIC +// #endif + +#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/include/topgg/client.h b/include/topgg/client.h index ed7085f..3abca0a 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @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-06-17 + * @date 2025-07-02 * @version 3.0.0 */ 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 9cb99ee..2ccbb84 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @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-06-17 + * @date 2025-07-02 * @version 3.0.0 */ @@ -54,6 +54,8 @@ namespace topgg { * @since 2.0.0 */ class TOPGG_EXPORT voter { + voter(const dpp::json& j); + public: voter() = delete; @@ -85,8 +87,6 @@ namespace topgg { */ time_t created_at; - voter(const dpp::json& j); - friend class client; }; diff --git a/include/topgg/result.h b/include/topgg/result.h index 636d005..dc8796a 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @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-06-17 + * @date 2025-07-02 * @version 3.0.0 */ diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index d17dcc0..6cc4ecc 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,36 +4,15 @@ * @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-06-17 + * @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 - -#if defined(__GNUC__) || defined(__clang__) -#define TOPGG_UNUSED __attribute__((unused)) -#else -#define TOPGG_UNUSED -#endif +#define __TOPGG_API__ +#include +#undef __TOPGG_API__ #ifdef __TOPGG_TESTING__ #define TOPGG_AUTOPOSTER_MIN_INTERVAL 5 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/models.cpp b/src/models.cpp index 5e13ef8..b44d8d3 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -1,9 +1,5 @@ #include -using topgg::bot; -using topgg::bot_query; -using topgg::voter; - #ifdef _WIN32 #include #include @@ -19,51 +15,51 @@ static void strptime(const char* s, const char* f, tm* t) { #endif #endif -#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({ \ +#define _TOPGG_DESERIALIZE_OPTIONAL_STRING_ALIAS(j, name, prop) \ + _TOPGG_IGNORE_EXCEPTION({ \ const auto value{j[#name].template get()}; \ \ if (value.size() > 0) { \ @@ -71,30 +67,34 @@ static void strptime(const char* s, const char* f, tm* t) { } \ }) -#define SNOWFLAKE_FROM_JSON(j, name) \ +#define _TOPGG_SNOWFLAKE_FROM_JSON(j, name) \ dpp::snowflake{j[#name].template get()} +using topgg::bot; +using topgg::bot_query; +using topgg::voter; + static time_t timestamp_from_id(const dpp::snowflake& id) { return static_cast(((id >> 22) / 1000) + 1420070400); } bot::bot(const dpp::json& j) { - id = SNOWFLAKE_FROM_JSON(j, clientid); - topgg_id = SNOWFLAKE_FROM_JSON(j, id); + id = _TOPGG_SNOWFLAKE_FROM_JSON(j, clientid); + topgg_id = _TOPGG_SNOWFLAKE_FROM_JSON(j, id); - DESERIALIZE(j, username, std::string); - DESERIALIZE(j, avatar, std::string); + _TOPGG_DESERIALIZE(j, username, std::string); + _TOPGG_DESERIALIZE(j, avatar, std::string); created_at = timestamp_from_id(id); - 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); + _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); - IGNORE_EXCEPTION({ + _TOPGG_IGNORE_EXCEPTION({ const auto j_owners{j["owners"].template get>()}; owners.reserve(j_owners.size()); @@ -105,22 +105,22 @@ bot::bot(const dpp::json& j) { }); const auto j_submitted_at{j["date"].template get()}; - tm submitted_at_tm; + tm submitted_at_tm{}; strptime(j_submitted_at.data(), "%Y-%m-%dT%H:%M:%S", &submitted_at_tm); submitted_at = mktime(&submitted_at_tm); - DESERIALIZE_ALIAS(j, points, votes, size_t); - DESERIALIZE_ALIAS(j, monthlyPoints, monthly_votes, size_t); - DESERIALIZE_OPTIONAL(j, invite, std::string); - DESERIALIZE_OPTIONAL(j, vanity, std::string); - DESERIALIZE_OPTIONAL(j, support, std::string); - DESERIALIZE_OPTIONAL(j, server_count, size_t); + _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(reviews, averageScore, review_score, double); - DESERIALIZE_ALIAS(reviews, count, review_count, size_t); + _TOPGG_DESERIALIZE_ALIAS(reviews, averageScore, review_score, double); + _TOPGG_DESERIALIZE_ALIAS(reviews, count, review_count, size_t); } static std::string querystring(const std::string& value) { @@ -186,10 +186,10 @@ dpp::async> bot_query::co_send() { #endif voter::voter(const dpp::json& j) { - id = SNOWFLAKE_FROM_JSON(j, id); + id = _TOPGG_SNOWFLAKE_FROM_JSON(j, id); - DESERIALIZE(j, username, std::string); - DESERIALIZE(j, avatar, std::string); + _TOPGG_DESERIALIZE(j, username, std::string); + _TOPGG_DESERIALIZE(j, avatar, std::string); created_at = timestamp_from_id(id); } 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..4a162de --- /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} PRIVATE /nologo $<$:/diagnostics:caret /MDd /DDEBUG /D_DEBUG> $<$:/MD /O2 /Oi /Oy /Gy /DNDEBUG>) +else() +target_compile_options(${FILENAME} PRIVATE $<$:-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/test.cpp b/tests/test_api.cpp similarity index 97% rename from test.cpp rename to tests/test_api.cpp index b95d6cd..fbcfc9a 100644 --- a/test.cpp +++ b/tests/test_api.cpp @@ -1,7 +1,3 @@ -#ifndef __TOPGG_TESTING__ -#define __TOPGG_TESTING__ -#endif - #include #include diff --git a/test_autoposter.cpp b/tests/test_autoposter.cpp similarity index 95% rename from test_autoposter.cpp rename to tests/test_autoposter.cpp index 2bd7164..0f96154 100644 --- a/test_autoposter.cpp +++ b/tests/test_autoposter.cpp @@ -1,7 +1,3 @@ -#ifndef __TOPGG_TESTING__ -#define __TOPGG_TESTING__ -#endif - #include #include From 2d9027fc27d29cf367bf375908ad612bdd860657 Mon Sep 17 00:00:00 2001 From: null8626 Date: Thu, 11 Sep 2025 19:35:15 +0700 Subject: [PATCH 41/41] feat: adapt to v1 --- CMakeLists.txt | 6 +- README.md | 146 +++++-- deps/drogon | 1 - include/topgg/client.h | 358 +++++++++++++----- include/topgg/export.h | 2 +- include/topgg/models.h | 111 +++++- include/topgg/result.h | 29 +- include/topgg/topgg.h | 8 +- include/topgg/webhooks/cpp-httplib.h | 4 +- include/topgg/webhooks/drogon.h | 4 +- include/topgg/webhooks/models.h | 10 +- src/client.cpp | 173 ++++++--- src/models.cpp | 44 ++- src/result.cpp | 43 ++- src/webhooks/models.cpp | 2 +- tests/CMakeLists.txt | 10 +- tests/cpp-httplib-webhooks/test_vote.cpp | 8 +- tests/drogon-webhooks/test_vote.cpp | 8 +- tests/test_api.cpp | 31 +- ...autoposter.cpp => test_bot_autoposter.cpp} | 2 +- 20 files changed, 732 insertions(+), 268 deletions(-) delete mode 160000 deps/drogon rename tests/{test_autoposter.cpp => test_bot_autoposter.cpp} (95%) diff --git a/CMakeLists.txt b/CMakeLists.txt index 31ca8c4..75dbcbb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -51,7 +51,7 @@ set(TOPGG_CXX_STANDARD 17) endif() if(TESTING) -target_compile_definitions(topgg PRIVATE __TOPGG_TESTING__) +target_compile_definitions(topgg PUBLIC __TOPGG_TESTING__) endif() set_target_properties(topgg PROPERTIES @@ -116,7 +116,7 @@ set(USE_SUBMODULE ON) add_subdirectory(deps/drogon) -target_compile_definitions(topgg PRIVATE __TOPGG_DROGON_WEBHOOKS__) +target_compile_definitions(topgg PUBLIC __TOPGG_DROGON_WEBHOOKS__) if(WIN32) target_compile_definitions(topgg PRIVATE _CRT_SECURE_NO_WARNINGS) @@ -124,6 +124,8 @@ cmake_policy(SET CMP0091 NEW) endif() endif() +target_compile_definitions(topgg PRIVATE __TOPGG_BUILDING__) + if(MSVC) target_compile_options(topgg PRIVATE /nologo $<$:/diagnostics:caret /MDd /DDEBUG /D_DEBUG> $<$:/MD /O2 /Oi /Oy /Gy /DNDEBUG>) else() diff --git a/README.md b/README.md index 95ef02c..2c4a87b 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,26 @@ The community-maintained C++17 library for Top.gg. +## Chapters + +- [Building from source](#building-from-source) + - [Main API wrapper](#main-api-wrapper) + - [Webhooks only](#webhooks-only) +- [Setting up](#setting-up) +- [Usage](#usage) + - [Getting a bot](#getting-a-bot) + - [Getting several bots](#getting-several-bots) + - [Getting your project's voters](#getting-your-projects-voters) + - [Getting your project's vote information of a user](#getting-your-projects-vote-information-of-a-user) + - [Getting your bot's server count](#getting-your-bots-server-count) + - [Posting your bot's server count](#posting-your-bots-server-count) + - [Posting your bot's application commands list](#posting-your-bots-application-commands-list) + - [Automatically posting your bot's server count every few minutes](#automatically-posting-your-bots-server-count-every-few-minutes) + - [Checking if the weekend vote multiplier is active](#checking-if-the-weekend-vote-multiplier-is-active) + - [Generating widget URLs](#generating-widget-urls) + - [Webhooks](#webhooks) + - [Being notified whenever someone voted for your project](#being-notified-whenever-someone-voted-for-your-project) + ## Building from source First, clone the git repository like so: @@ -323,7 +343,7 @@ try { } ``` -### Getting your bot's voters +### Getting your project's voters #### With C++17 callbacks @@ -381,15 +401,43 @@ try { } ``` -### Check if a user has voted for your bot +### Getting your project's vote information of a user #### With C++17 callbacks +##### Discord ID + ```cpp -client.has_voted(661200758510977084, [](const auto& result) { +client.get_vote(661200758510977084, TOPGG_USER_SOURCE_DISCORD, [](const auto& result) { try { - if (result.get()) { - std::cout << "Checks out." << std::endl; + const auto vote{result.get()}; + + if (vote.has_value()) { + const auto data{vote.value()}; + + std::cout << "User has voted with a weight of " << data.weight << '.' << std::endl; + } else { + std::cout << "User has not voted." << std::endl; + } + } catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; + } +}); +``` + +##### Top.gg ID + +```cpp +client.get_vote(8226924471638491136, TOPGG_USER_SOURCE_TOPGG, [](const auto& result) { + try { + const auto vote{result.get()}; + + if (vote.has_value()) { + const auto data{vote.value()}; + + std::cout << "User has voted with a weight of " << data.weight << '.' << std::endl; + } else { + std::cout << "User has not voted." << std::endl; } } catch (const std::exception& exc) { std::cerr << "error: " << exc.what() << std::endl; @@ -399,12 +447,36 @@ client.has_voted(661200758510977084, [](const auto& result) { #### With C++20 coroutines +##### Discord ID + ```cpp try { - const auto voted = co_await client.co_has_voted(661200758510977084); + const auto vote{co_await client.co_get_vote(661200758510977084, TOPGG_USER_SOURCE_DISCORD)}; - if (voted) { - std::cout << "Checks out." << std::endl; + if (vote.has_value()) { + const auto data{vote.value()}; + + std::cout << "User has voted with a weight of " << data.weight << '.' << std::endl; + } else { + std::cout << "User has not voted." << std::endl; + } +} catch (const std::exception& exc) { + std::cerr << "error: " << exc.what() << std::endl; +} +``` + +##### Top.gg ID + +```cpp +try { + const auto vote{co_await client.co_get_vote(8226924471638491136, TOPGG_USER_SOURCE_TOPGG)}; + + if (vote.has_value()) { + const auto data{vote.value()}; + + std::cout << "User has voted with a weight of " << data.weight << '.' << std::endl; + } else { + std::cout << "User has not voted." << std::endl; } } catch (const std::exception& exc) { std::cerr << "error: " << exc.what() << std::endl; @@ -416,7 +488,7 @@ try { #### With C++17 callbacks ```cpp -client.get_server_count([](const auto& result) { +client.get_bot_server_count([](const auto& result) { try { auto server_count = result.get(); @@ -431,7 +503,7 @@ client.get_server_count([](const auto& result) { ```cpp try { - const auto server_count = co_await client.co_get_server_count(); + const auto server_count = co_await client.co_get_bot_server_count(); std::cout << server_count.value_or(0) << std::endl; } catch (const std::exception& exc) { @@ -444,7 +516,7 @@ try { #### With C++17 callbacks ```cpp -client.post_server_count([](const auto success) { +client.post_bot_server_count([](const auto success) { if (success) { std::cout << "Stats posted!" << std::endl; } @@ -454,25 +526,47 @@ client.post_server_count([](const auto success) { #### With C++20 coroutines ```cpp -const auto success = co_await client.co_post_server_count(); +const auto success = co_await client.co_post_bot_server_count(); if (success) { std::cout << "Stats posted!" << std::endl; } ``` +### Posting your bot's application commands list + +#### With C++17 callbacks + +```cpp +client.post_bot_commands([](const auto success) { + if (success) { + std::cout << "Discord bot commands posted!" << std::endl; + } +}); +``` + +#### With C++20 coroutines + +```cpp +const auto success{co_await client.co_post_bot_commands()}; + +if (success) { + std::cout << "Discord bot commands posted!" << std::endl; +} +``` + ### Automatically posting your bot's server count every few minutes #### Without a callback ```cpp -client.start_autoposter(); +client.start_bot_autoposter(); ``` #### With a callback ```cpp -client.start_autoposter([](const auto& result) { +client.start_bot_autoposter([](const auto& result) { if (result) { std::cout << "Successfully posted " << *result << " servers to Top.gg!" << std::endl; } @@ -482,14 +576,14 @@ client.start_autoposter([](const auto& result) { #### From a custom source ```cpp -class my_autoposter_source: private topgg::autoposter_source { +class my_bot_autoposter_source: private topgg::bot_autoposter_source { public: - virtual size_t get_server_count(dpp::cluster& bot) { + virtual size_t get_bot_server_count(dpp::cluster& bot) { return ...; } }; -client.start_autoposter(reinterpret_cast(new my_autoposter_source), [](const auto& result) { +client.start_bot_autoposter(reinterpret_cast(new my_bot_autoposter_source), [](const auto& result) { if (result) { std::cout << "Successfully posted " << *result << " servers to Top.gg!" << std::endl; } @@ -554,7 +648,7 @@ const auto widget_url{topgg::widget::social(TOPGG_WIDGET_DISCORD_BOT, 2648116137 ### Webhooks -#### Being notified whenever someone voted for your bot +#### Being notified whenever someone voted for your project ##### cpp-httplib @@ -572,13 +666,13 @@ const auto widget_url{topgg::widget::social(TOPGG_WIDGET_DISCORD_BOT, 2648116137 template using cpp_httplib_webhook = topgg::webhook::cpp_httplib; -using topgg::webhook::vote; +using topgg::webhook::vote_event; -class my_vote_listener: public cpp_httplib_webhook { +class my_vote_listener: public cpp_httplib_webhook { public: - inline my_vote_listener(const std::string& authorization): cpp_httplib_webhook(authorization) {} + inline my_vote_listener(const std::string& authorization): cpp_httplib_webhook(authorization) {} - void callback(const vote& v) override { + void callback(const vote_event& v) override { std::cout << "A user with the ID of " << v.voter_id << " has voted us on Top.gg!" << std::endl; } }; @@ -617,11 +711,11 @@ int main() { template using drogon_webhook = topgg::webhook::drogon; -using topgg::webhook::vote; +using topgg::webhook::vote_event; -class my_vote_listener: public ::drogon::HttpSimpleController, public drogon_webhook { +class my_vote_listener: public ::drogon::HttpSimpleController, public drogon_webhook { public: - inline my_vote_listener(const std::string& authorization): drogon_webhook(authorization) {} + inline my_vote_listener(const std::string& authorization): drogon_webhook(authorization) {} TOPGG_DROGON_WEBHOOK(); @@ -629,7 +723,7 @@ public: PATH_ADD("/votes", ::drogon::Post); PATH_LIST_END - void callback(const vote& v) override { + void callback(const vote_event& v) override { std::cout << "A user with the ID of " << v.voter_id << " has voted us on Top.gg!" << std::endl; } }; diff --git a/deps/drogon b/deps/drogon deleted file mode 160000 index 8079e76..0000000 --- a/deps/drogon +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 8079e76aef32d6ea8b74887490dbea246731ddaf diff --git a/include/topgg/client.h b/include/topgg/client.h index 3abca0a..7ec26ed 100644 --- a/include/topgg/client.h +++ b/include/topgg/client.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ @@ -27,12 +27,12 @@ namespace topgg { using get_bot_completion_event = std::function&)>; /** - * @brief The callback function to call when get_server_count completes. + * @brief The callback function to call when get_bot_server_count completes. * - * @see topgg::client::get_server_count + * @see topgg::client::get_bot_server_count * @since 3.0.0 */ - using get_server_count_completion_event = std::function>&)>; + using get_bot_server_count_completion_event = std::function>&)>; /** * @brief The callback function to call when get_voters completes. @@ -50,6 +50,14 @@ namespace topgg { */ using has_voted_completion_event = std::function&)>; + /** + * @brief The callback function to call when get_vote completes. + * + * @see topgg::client::get_vote + * @since 3.0.0 + */ + using get_vote_completion_event = std::function>&)>; + /** * @brief The callback function to call when is_weekend completes. * @@ -59,21 +67,29 @@ namespace topgg { using is_weekend_completion_event = std::function&)>; /** - * @brief The callback function to call when post_server_count completes. + * @brief The callback function to call when post_bot_commands completes. * - * @see topgg::client::post_server_count + * @see topgg::client::post_bot_commands * @since 3.0.0 */ - using post_server_count_completion_event = std::function; + using post_bot_commands_completion_event = std::function; /** - * @brief The callback function to call after every autopost request to the API, successful or not. + * @brief The callback function to call when post_bot_server_count completes. * - * @see topgg::client::start_autoposter - * @see topgg::client::stop_autoposter + * @see topgg::client::post_bot_server_count * @since 3.0.0 */ - using autopost_completion_event = std::function&)>; + using post_bot_server_count_completion_event = std::function; + + /** + * @brief The callback function to call after every bot autopost request to the API, successful or not. + * + * @see topgg::client::start_bot_autoposter + * @see topgg::client::stop_bot_autoposter + * @since 3.0.0 + */ + using bot_autopost_completion_event = std::function&)>; /** * @brief Interact with the API's endpoints. @@ -81,28 +97,32 @@ namespace topgg { * @since 2.0.0 */ class TOPGG_EXPORT client { - std::multimap m_headers; - std::string m_token; - std::string m_id; + dpp::http_headers m_headers; + dpp::snowflake m_id; + bool m_legacy; dpp::cluster& m_cluster; - dpp::timer m_autoposter_timer; + dpp::timer m_bot_autoposter_timer; + void request(const dpp::http_method method, const std::string& url, const dpp::http_completion_event callback, const std::string& body = "") { + m_cluster.request(TOPGG_BASE_URL + url, method, callback, body, "application/json", m_headers); + } + template - void basic_request(const std::string& url, const std::function&)>& callback, std::function&& conversion_fn) { - 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); + void basic_request(const dpp::http_method method, const std::string& url, const std::function&)>& callback, std::function&& conversion_fn, const std::string& body = "") { + request(method, url, [callback, conversion_fn_in = std::move(conversion_fn)](const dpp::http_request_completion_t& response) { callback(result{response, conversion_fn_in}); }, body); } - size_t get_server_count(); - void post_server_count_inner(const size_t server_count, dpp::http_completion_event callback); + size_t get_bot_server_count(); + void post_bot_server_count_inner(const size_t server_count, const dpp::http_completion_event callback); public: client() = delete; /** - * @brief Constructs the client class. + * @brief Creates a client object. * * @param cluster A pointer to the bot's D++ cluster using this library. - * @param token The API token to use. To retrieve it, see https://github.com/top-gg/rust-sdk/assets/60427892/d2df5bd3-bc48-464c-b878-a04121727bff. + * @param token The API token to use. * @since 2.0.0 */ client(dpp::cluster& cluster, const std::string& token); @@ -152,7 +172,7 @@ namespace topgg { * * client.get_bot(264811613708746752, [](const auto& result) { * try { - * const auto topgg_bot = result.get(); + * const auto topgg_bot{result.get()}; * * std::cout << topgg_bot.username << std::endl; * } catch (const std::exception& exc) { @@ -182,7 +202,7 @@ namespace topgg { * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto topgg_bot = co_await 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) { @@ -222,7 +242,7 @@ namespace topgg { * .sort_by_monthly_votes() * .send([](const auto& result) { * try { - * const auto bots = result.get(); + * const auto bots{result.get()}; * * for (const auto& bot: bots) { * std::cout << bot.username << std::endl; @@ -272,9 +292,9 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.get_server_count([](const auto& result) { + * client.get_bot_server_count([](const auto& result) { * try { - * auto server_count = result.get(); + * const auto server_count{result.get()}; * * std::cout << server_count.value_or(0) << std::endl; * } catch (const std::exception& exc) { @@ -283,14 +303,14 @@ namespace topgg { * }); * ``` * - * @param callback The callback function to call when get_server_count completes. - * @note For its C++20 coroutine counterpart, see co_get_server_count. + * @param callback The callback function to call when get_bot_server_count completes. + * @note For its C++20 coroutine counterpart, see co_get_bot_server_count. * @see topgg::result - * @see topgg::client::start_autoposter - * @see topgg::client::co_get_server_count + * @see topgg::client::start_bot_autoposter + * @see topgg::client::co_get_bot_server_count * @since 3.0.0 */ - void get_server_count(const get_server_count_completion_event& callback); + void get_bot_server_count(const get_bot_server_count_completion_event& callback); #ifdef DPP_CORO /** @@ -303,7 +323,7 @@ namespace topgg { * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto server_count = co_await client.co_get_server_count(); + * const auto server_count{co_await client.co_get_bot_server_count()}; * * std::cout << server_count.value_or(0) << std::endl; * } catch (const std::exception& exc) { @@ -316,17 +336,17 @@ namespace topgg { * @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. + * @note For its C++17 callback-based counterpart, see get_bot_server_count. * @see topgg::async_result - * @see topgg::client::start_autoposter - * @see topgg::client::get_server_count + * @see topgg::client::start_bot_autoposter + * @see topgg::client::get_bot_server_count * @since 3.0.0 */ - topgg::async_result> co_get_server_count(); + topgg::async_result> co_get_bot_server_count(); #endif /** - * @brief Fetches your Discord bot's recent 100 unique voters. + * @brief Fetches your project's recent 100 unique voters. * * Example: * @@ -336,7 +356,7 @@ namespace topgg { * * client.get_voters(1, [](const auto& result) { * try { - * auto voters = result.get(); + * const auto voters{result.get()}; * * for (auto& voter: voters) { * std::cout << voter.username << std::endl; @@ -352,14 +372,14 @@ namespace topgg { * @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::start_bot_autoposter * @see topgg::client::co_get_voters * @since 2.0.0 */ void get_voters(size_t page, const get_voters_completion_event& callback); /** - * @brief Fetches your Discord bot's recent 100 unique voters. + * @brief Fetches your project's recent 100 unique voters. * * Example: * @@ -369,7 +389,7 @@ namespace topgg { * * client.get_voters([](const auto& result) { * try { - * auto voters = result.get(); + * const auto voters{result.get()}; * * for (auto& voter: voters) { * std::cout << voter.username << std::endl; @@ -384,7 +404,7 @@ namespace topgg { * @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::start_bot_autoposter * @see topgg::client::co_get_voters * @since 2.0.0 */ @@ -392,7 +412,7 @@ namespace topgg { #ifdef DPP_CORO /** - * @brief Fetches your Discord bot's recent 100 unique voters through a C++20 coroutine. + * @brief Fetches your project's recent 100 unique voters through a C++20 coroutine. * * Example: * @@ -401,7 +421,7 @@ namespace topgg { * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto voters = co_await client.co_get_voters(); + * const auto voters{co_await client.co_get_voters()}; * * for (const auto& voter: voters) { * std::cout << voter.username << std::endl; @@ -420,7 +440,7 @@ namespace topgg { * @note For its C++17 callback-based counterpart, see get_voters. * @see topgg::async_result * @see topgg::voter - * @see topgg::client::start_autoposter + * @see topgg::client::start_bot_autoposter * @see topgg::client::get_voters * @since 2.0.0 */ @@ -428,7 +448,8 @@ namespace topgg { #endif /** - * @brief Checks if the specified Discord user has voted your Discord bot. + * @deprecated Use a v1 API token with `get_vote()` instead. + * @brief Checks if the specified Top.gg user has voted for your project in the past 12 hours. * * Example: * @@ -436,7 +457,7 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.has_voted(661200758510977084, [](const auto& result) { + * client.has_voted(8226924471638491136, [](const auto& result) { * try { * if (result.get()) { * std::cout << "Checks out." << std::endl; @@ -451,15 +472,16 @@ namespace topgg { * @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::client::start_autoposter - * @note For its C++20 coroutine counterpart, see co_has_voted. + * @see topgg::client::start_bot_autoposter * @since 2.0.0 */ + [[deprecated("`has_voted()` is deprecated. Use a v1 API token with `get_vote()` instead.")]] void has_voted(const dpp::snowflake user_id, const has_voted_completion_event& callback); #ifdef DPP_CORO /** - * @brief Checks if the specified Discord user has voted your Discord bot through a C++20 coroutine. + * @deprecated Use a v1 API token with `co_get_vote()` instead. + * @brief Checks if the specified Top.gg user has voted for your project in the past 12 hours through a C++20 coroutine. * * Example: * @@ -468,7 +490,7 @@ namespace topgg { * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto voted = co_await client.co_has_voted(661200758510977084); + * const auto voted{co_await client.co_has_voted(8226924471638491136)}; * * if (voted) { * std::cout << "Checks out." << std::endl; @@ -487,13 +509,96 @@ namespace topgg { * @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::client::start_autoposter + * @see topgg::client::start_bot_autoposter * @see topgg::client::has_voted * @since 2.0.0 */ + [[deprecated("`co_has_voted()` is deprecated. Use a v1 API token with `co_get_vote()` instead.")]] topgg::async_result co_has_voted(const dpp::snowflake user_id); #endif + /** + * @brief Fetches the latest vote information of a Top.gg user on your project. + * + * Example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client client{bot, "your top.gg token"}; + * + * client.get_vote(661200758510977084, TOPGG_USER_SOURCE_DISCORD, [](const auto& result) { + * try { + * const auto vote{result.get()}; + * + * if (vote.has_value()) { + * const auto data{vote.value()}; + * + * std::cout << "User has voted with a weight of " << data.weight << '.' << std::endl; + * } else { + * std::cout << "User has not voted." << std::endl; + * } + * } catch (const std::exception& exc) { + * std::cerr << "error: " << exc.what() << std::endl; + * } + * }); + * ``` + * + * @param user_id The requested user's ID. + * @param user_source The ID type to use. + * @param callback The callback function to call when get_vote completes. + * @note For its C++20 coroutine counterpart, see co_get_vote. + * @see TOPGG_USER_SOURCE_DISCORD + * @see TOPGG_USER_SOURCE_TOPGG + * @see topgg::result + * @see topgg::client::start_bot_autoposter + * @since 3.0.0 + */ + void get_vote(const dpp::snowflake user_id, const char* user_source, const get_vote_completion_event& callback); + +#ifdef DPP_CORO + /** + * @brief Fetches the latest vote information of a Top.gg user on your project through a C++20 coroutine. + * + * Example: + * + * ```cpp + * dpp::cluster bot{"your bot token"}; + * topgg::client client{bot, "your top.gg token"}; + * + * try { + * const auto vote{co_await client.co_get_vote(661200758510977084, TOPGG_USER_SOURCE_DISCORD)}; + * + * if (vote.has_value()) { + * const auto data{vote.value()}; + * + * std::cout << "User has voted with a weight of " << data.weight << '.' << std::endl; + * } else { + * std::cout << "User has not voted." << std::endl; + * } + * } catch (const std::exception& exc) { + * std::cerr << "error: " << exc.what() << std::endl; + * } + * ``` + * + * @param user_id The requested user's ID. + * @param user_source The ID type to use. + * @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 get_vote. + * @see TOPGG_USER_SOURCE_DISCORD + * @see TOPGG_USER_SOURCE_TOPGG + * @see topgg::async_result + * @see topgg::client::start_bot_autoposter + * @see topgg::client::get_vote + * @since 3.0.0 + */ + topgg::async_result> co_get_vote(const dpp::snowflake user_id, const char* user_source); +#endif + /** * @brief Checks if the weekend multiplier is active, where a single vote counts as two. * @@ -533,7 +638,7 @@ namespace topgg { * topgg::client client{bot, "your top.gg token"}; * * try { - * const auto is_weekend = co_await 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; @@ -565,21 +670,72 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.post_server_count([](const auto success) { + * client.post_bot_commands([](const auto success) { + * if (success) { + * std::cout << "Discord bot commands posted!" << std::endl; + * } + * }); + * ``` + * + * @param callback The callback function to call when post_bot_commands completes. + * @note For its C++20 coroutine counterpart, see co_post_bot_commands. + * @see topgg::result + * @see topgg::client::start_bot_autoposter + * @see topgg::client::co_post_bot_commands + * @since 3.0.0 + */ + void post_bot_commands(const post_bot_commands_completion_event& callback); + +#ifdef DPP_CORO + /** + * @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 client{bot, "your top.gg token"}; + * + * const auto success{co_await client.co_post_bot_commands()}; + * + * if (success) { + * std::cout << "Discord bot commands posted!" << std::endl; + * } + * ``` + * + * @return co_await to retrieve a bool + * @note For its C++17 callback-based counterpart, see post_bot_commands. + * @see topgg::client::start_bot_autoposter + * @see topgg::client::post_bot_commands + * @since 3.0.0 + */ + dpp::async co_post_bot_commands(); +#endif + + /** + * @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 client{bot, "your top.gg token"}; + * + * client.post_bot_server_count([](const auto success) { * if (success) { * std::cout << "Stats posted!" << std::endl; * } * }); * ``` * - * @param callback The callback function to call when post_server_count completes. - * @note For its C++20 coroutine counterpart, see co_post_server_count. + * @param callback The callback function to call when post_bot_server_count completes. + * @note For its C++20 coroutine counterpart, see co_post_bot_server_count. * @see topgg::result - * @see topgg::client::start_autoposter - * @see topgg::client::co_post_server_count + * @see topgg::client::start_bot_autoposter + * @see topgg::client::co_post_bot_server_count * @since 3.0.0 */ - void post_server_count(const post_server_count_completion_event& callback); + void post_bot_server_count(const post_bot_server_count_completion_event& callback); #ifdef DPP_CORO /** @@ -591,7 +747,7 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * const auto success = co_await client.co_post_server_count(); + * const auto success{co_await client.co_post_bot_server_count()}; * * if (success) { * std::cout << "Stats posted!" << std::endl; @@ -599,12 +755,12 @@ namespace topgg { * ``` * * @return co_await to retrieve a bool - * @note For its C++17 callback-based counterpart, see post_server_count. - * @see topgg::client::start_autoposter - * @see topgg::client::post_server_count + * @note For its C++17 callback-based counterpart, see post_bot_server_count. + * @see topgg::client::start_bot_autoposter + * @see topgg::client::post_bot_server_count * @since 3.0.0 */ - dpp::async co_post_server_count(); + dpp::async co_post_bot_server_count(); #endif /** @@ -616,7 +772,7 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.start_autoposter([](const auto& result) { + * client.start_bot_autoposter([](const auto& result) { * if (result) { * std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; * } @@ -625,13 +781,13 @@ namespace topgg { * * @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 + * @note This function has no effect if the bot autoposter is already running. + * @see topgg::client::post_bot_server_count + * @see topgg::client::stop_bot_autoposter + * @see topgg::bot_autopost_completion_event * @since 2.0.0 */ - void start_autoposter(const autopost_completion_event& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); + void start_bot_autoposter(const bot_autopost_completion_event& callback, time_t interval = TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL); /** * @brief Starts autoposting your Discord bot's server count using data directly from your D++ cluster instance. @@ -642,16 +798,16 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.start_autoposter(); + * client.start_bot_autoposter(); * ``` * * @param interval The interval between posting in seconds. Defaults to 15 minutes. - * @note This function has no effect if the autoposter is already running. - * @see topgg::client::post_server_count - * @see topgg::client::stop_autoposter + * @note This function has no effect if the bot autoposter is already running. + * @see topgg::client::post_bot_server_count + * @see topgg::client::stop_bot_autoposter * @since 2.0.0 */ - void start_autoposter(time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); + void start_bot_autoposter(time_t interval = TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL); /** * @brief Starts autoposting your Discord bot's server count using a custom data source. @@ -659,9 +815,9 @@ namespace topgg { * Example: * * ```cpp - * class my_autoposter_source: private topgg::autoposter_source { + * class my_bot_autoposter_source: private topgg::bot_autoposter_source { * public: - * virtual size_t get_server_count(dpp::cluster& bot) { + * virtual size_t get_bot_server_count(dpp::cluster& bot) { * return ...; * } * }; @@ -669,24 +825,24 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.start_autoposter(reinterpret_cast(new my_autoposter_source), [](const auto& result) { + * client.start_bot_autoposter(reinterpret_cast(new my_bot_autoposter_source), [](const auto& result) { * if (result) { * std::cout << "Successfully posted " << *result << " servers to the API!" << std::endl; * } * }); * ``` * - * @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 source A pointer to a bot autoposter source located in the heap memory. This pointer must be allocated with new, and it will be deleted once the bot 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_server_count - * @see topgg::client::stop_autoposter - * @see topgg::autopost_completion_event - * @see topgg::autoposter_source + * @note This function has no effect if the bot autoposter is already running. + * @see topgg::client::post_bot_server_count + * @see topgg::client::stop_bot_autoposter + * @see topgg::bot_autopost_completion_event + * @see topgg::bot_autoposter_source * @since 2.0.0 */ - void start_autoposter(autoposter_source* source, const autopost_completion_event& callback, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); + void start_bot_autoposter(bot_autoposter_source* source, const bot_autopost_completion_event& callback, time_t interval = TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL); /** * @brief Starts autoposting your Discord bot's server count using a custom data source. @@ -694,9 +850,9 @@ namespace topgg { * Example: * * ```cpp - * class my_autoposter_source: private topgg::autoposter_source { + * class my_bot_autoposter_source: private topgg::bot_autoposter_source { * public: - * virtual size_t get_server_count(dpp::cluster& bot) { + * virtual size_t get_bot_server_count(dpp::cluster& bot) { * return ...; * } * }; @@ -704,21 +860,21 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.start_autoposter(reinterpret_cast(new my_autoposter_source)); + * client.start_bot_autoposter(reinterpret_cast(new my_bot_autoposter_source)); * ``` * - * @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 source A pointer to a bot autoposter source located in the heap memory. This pointer must be allocated with new, and it will be deleted once the bot 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::client::post_server_count - * @see topgg::client::stop_autoposter - * @see topgg::autoposter_source + * @note This function has no effect if the bot autoposter is already running. + * @see topgg::client::post_bot_server_count + * @see topgg::client::stop_bot_autoposter + * @see topgg::bot_autoposter_source * @since 3.0.0 */ - void start_autoposter(autoposter_source* source, time_t interval = TOPGG_AUTOPOSTER_MIN_INTERVAL); + void start_bot_autoposter(bot_autoposter_source* source, time_t interval = TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL); /** - * @brief Prematurely stops the autoposter. Calling this function is usually unnecessary as this function will be called in the destructor. + * @brief Prematurely stops the bot autoposter. Calling this function is usually unnecessary as this function will be called in the destructor. * * Example: * @@ -726,21 +882,21 @@ namespace topgg { * dpp::cluster bot{"your bot token"}; * topgg::client client{bot, "your top.gg token"}; * - * client.start_autoposter(); + * client.start_bot_autoposter(); * * // ... * - * client.stop_autoposter(); + * client.stop_bot_autoposter(); * ``` * - * @note This function has no effect if the autoposter is already stopped. - * @see topgg::client::post_server_count + * @note This function has no effect if the bot autoposter is already stopped. + * @see topgg::client::post_bot_server_count * @since 2.0.0 */ - void stop_autoposter() noexcept; + void stop_bot_autoposter() noexcept; /** - * @brief The destructor. Stops the autoposter if it's running. + * @brief The destructor. Stops the bot autoposter if it's running. */ ~client(); diff --git a/include/topgg/export.h b/include/topgg/export.h index cb6ab47..5406509 100644 --- a/include/topgg/export.h +++ b/include/topgg/export.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ diff --git a/include/topgg/models.h b/include/topgg/models.h index 2ccbb84..fbf59bf 100644 --- a/include/topgg/models.h +++ b/include/topgg/models.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ @@ -27,9 +27,53 @@ #undef _XOPEN_SOURCE #endif +/** + * @brief Generates a widget for Discord bots. + * + * @see TOPGG_WIDGET_DISCORD_SERVER + * @see topgg::widget::large + * @see topgg::widget::votes + * @see topgg::widget::owner + * @see topgg::widget::social + * @since 3.0.0 + */ #define TOPGG_WIDGET_DISCORD_BOT "discord/bot" + +/** + * @brief Generates a widget for Discord servers. + * + * @see TOPGG_WIDGET_DISCORD_BOT + * @see topgg::widget::large + * @see topgg::widget::votes + * @see topgg::widget::owner + * @see topgg::widget::social + * @since 3.0.0 + */ #define TOPGG_WIDGET_DISCORD_SERVER "discord/server" +/** + * @brief Use a Discord ID. + * + * @see TOPGG_USER_SOURCE_TOPGG + * @see topgg::client::get_vote + * @since 3.0.0 + */ +#define TOPGG_USER_SOURCE_DISCORD "discord" + +/** + * @brief Use a Top.gg ID. + * + * @see TOPGG_USER_SOURCE_DISCORD + * @see topgg::client::get_vote + * @since 3.0.0 + */ +#define TOPGG_USER_SOURCE_TOPGG "topgg" + +#ifdef __TOPGG_BUILDING__ +#define _TOPGG_SNOWFLAKE_FROM_JSON(j, name) \ + dpp::snowflake{j[#name].template get()} +#endif + #define TOPGG_BOT_QUERY_SORT(lib_name, api_name) \ inline bot_query& sort_by_##lib_name() noexcept { \ m_sort = #api_name; \ @@ -46,11 +90,62 @@ namespace topgg { class bot_query; class client; + /** + * @brief A Top.gg vote. + * + * @see topgg::voter + * @see topgg::client::get_vote + * @see topgg::client::get_voters + * @since 3.0.0 + */ + class TOPGG_EXPORT vote { + vote(const dpp::json& j); + + public: + /** + * @brief This vote's weight. + * + * @since 3.0.0 + */ + size_t weight; + + /** + * @brief When the vote was cast. + * + * @see topgg::vote::expired + * @since 3.0.0 + */ + time_t voted_at; + + /** + * @brief When the vote expires and the user is required to vote again. + * + * @see topgg::vote::expired + * @since 3.0.0 + */ + time_t expires_at; + + /** + * @brief Whether this vote is now expired. + * + * @return bool Whether this vote is now expired. + * @since 3.0.0 + */ + bool expired() const noexcept; + + inline operator bool() const noexcept { + return expired(); + } + + friend class client; + }; + /** * @brief A Top.gg voter. * + * @see topgg::vote + * @see topgg::client::get_vote * @see topgg::client::get_voters - * @see topgg::client::start_autoposter * @since 2.0.0 */ class TOPGG_EXPORT voter { @@ -60,7 +155,7 @@ namespace topgg { voter() = delete; /** - * @brief This voter's Discord ID. + * @brief This voter's ID. * * @since 2.0.0 */ @@ -345,7 +440,7 @@ namespace topgg { * .sort_by_monthly_votes() * .send([](const auto& result) { * try { - * const auto bots = result.get(); + * const auto bots{result.get()}; * * for (const auto& bot: bots) { * std::cout << bot.username << std::endl; @@ -409,12 +504,12 @@ namespace topgg { /** * @brief An abstract interface for bots that have a custom way of retrieving their server count. * - * @see topgg::start_autoposter + * @see topgg::start_bot_autoposter * @since 3.0.0 */ - class autoposter_source { + class bot_autoposter_source { public: - virtual size_t TOPGG_EXPORT get_server_count(dpp::cluster&) = 0; + virtual size_t TOPGG_EXPORT get_bot_server_count(dpp::cluster&) = 0; }; namespace widget { @@ -453,7 +548,7 @@ namespace topgg { std::string votes(const char* ty, const dpp::snowflake id); /** - * @brief Generates a small widget URL for displaying an entity's owner. + * @brief Generates a small widget URL for displaying a project's owner. * * Example: * diff --git a/include/topgg/result.h b/include/topgg/result.h index dc8796a..1684ef1 100644 --- a/include/topgg/result.h +++ b/include/topgg/result.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ @@ -41,6 +41,7 @@ namespace topgg { inline invalid_token() : std::invalid_argument("Invalid API token.") {} + friend class client; friend class internal_result; }; @@ -84,16 +85,14 @@ namespace topgg { class internal_result { const dpp::http_request_completion_t m_response; - void prepare() const; - - inline internal_result(const dpp::http_request_completion_t& response) - : m_response(response) {} + static void handle_response(const dpp::http_request_completion_t& response); - public: - internal_result() = delete; + inline internal_result(): m_response() {} + inline internal_result(const dpp::http_request_completion_t& response): m_response(response) {} template friend class result; + friend class client; }; class client; @@ -107,10 +106,10 @@ namespace topgg { template class result { const internal_result m_internal; - const std::function m_parse_fn; + const std::variant, T> m_data; - inline result(const dpp::http_request_completion_t& response, const std::function& parse_fn) - : m_internal(response), m_parse_fn(parse_fn) {} + inline result(const T& data): m_data(data) {} + inline result(const dpp::http_request_completion_t& response, const std::function& parse_fn): m_internal(response), m_data(parse_fn) {} public: result() = delete; @@ -127,9 +126,15 @@ namespace topgg { * @since 2.0.0 */ T get() const { - m_internal.prepare(); + try { + const auto parse_fn{std::get>(m_data)}; + + internal_result::handle_response(m_internal.m_response); - return m_parse_fn(dpp::json::parse(m_internal.m_response.body)); + return parse_fn(dpp::json::parse(m_internal.m_response.body)); + } catch (TOPGG_UNUSED const std::bad_variant_access&) { + return std::get(m_data); + } } friend class client; diff --git a/include/topgg/topgg.h b/include/topgg/topgg.h index 6cc4ecc..1d76266 100644 --- a/include/topgg/topgg.h +++ b/include/topgg/topgg.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ @@ -15,9 +15,9 @@ #undef __TOPGG_API__ #ifdef __TOPGG_TESTING__ -#define TOPGG_AUTOPOSTER_MIN_INTERVAL 5 +#define TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL 5 #else -#define TOPGG_AUTOPOSTER_MIN_INTERVAL 900 +#define TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL 900 #endif #ifdef __clang__ @@ -33,7 +33,7 @@ #pragma clang diagnostic pop #endif -#define TOPGG_BASE_URL "https://top.gg/api/v1" +#define TOPGG_BASE_URL "https://top.gg/api" #include #include diff --git a/include/topgg/webhooks/cpp-httplib.h b/include/topgg/webhooks/cpp-httplib.h index c223ce4..c1162ca 100644 --- a/include/topgg/webhooks/cpp-httplib.h +++ b/include/topgg/webhooks/cpp-httplib.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ @@ -40,7 +40,7 @@ namespace topgg { /** * @brief An abstract class that receives a Top.gg webhook event. Designed for use in cpp-httplib. * - * @see topgg::webhook::vote + * @see topgg::webhook::vote_event * @since 3.0.0 */ template diff --git a/include/topgg/webhooks/drogon.h b/include/topgg/webhooks/drogon.h index fff35a8..f4def45 100644 --- a/include/topgg/webhooks/drogon.h +++ b/include/topgg/webhooks/drogon.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ @@ -45,7 +45,7 @@ namespace topgg { /** * @brief An abstract class that receives a Top.gg webhook event. Designed for use as a drogon::HttpSimpleController in drogon. * - * @see topgg::webhook::vote + * @see topgg::webhook::vote_event * @since 3.0.0 */ template diff --git a/include/topgg/webhooks/models.h b/include/topgg/webhooks/models.h index a4e61d9..e06e363 100644 --- a/include/topgg/webhooks/models.h +++ b/include/topgg/webhooks/models.h @@ -4,7 +4,7 @@ * @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 + * @date 2025-09-12 * @version 3.0.0 */ @@ -34,14 +34,14 @@ namespace topgg { * * @since 3.0.0 */ - class vote { + class vote_event { public: - TOPGG_EXPORT vote(const json& j); + TOPGG_EXPORT vote_event(const json& j); - vote() = delete; + vote_event() = delete; /** - * @brief The ID of the Discord bot/server that received a vote. + * @brief The ID of the project that received a vote. * * @since 3.0.0 */ diff --git a/src/client.cpp b/src/client.cpp index a32b91e..e1472f7 100644 --- a/src/client.cpp +++ b/src/client.cpp @@ -67,20 +67,26 @@ static bool base64_decode(const std::string& input, std::string& output) { return true; } -static std::string id_from_bot_token(std::string bot_token) { - const auto pos{bot_token.find('.')}; +static dpp::json parse_api_token(const std::string& token) { + const auto first_pos{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)}; + if (first_pos != std::string::npos) { + const auto first_token_slice{token.substr(first_pos + 1)}; + const auto second_pos{first_token_slice.find('.')}; - for (size_t j{}; j < additional_equals; j++) { - base64_input.push_back('='); - } + if (second_pos != std::string::npos) { + auto base64_input{first_token_slice.substr(0, second_pos)}; + const auto additional_equals{4 - (base64_input.length() % 4)}; + + for (size_t i{}; i < additional_equals; i++) { + base64_input.push_back('='); + } + + std::string base64_decoded{}; - if (base64_decode(base64_input, decoded_base64)) { - return decoded_base64; + if (base64_decode(base64_input, base64_decoded)) { + return dpp::json::parse(base64_decoded); + } } } @@ -88,16 +94,19 @@ static std::string id_from_bot_token(std::string bot_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_cluster(cluster), m_bot_autoposter_timer(0) { + const auto token_data{parse_api_token(token)}; + + m_id = _TOPGG_SNOWFLAKE_FROM_JSON(token_data, id); + m_legacy = !token_data.contains("_t"); - m_headers.insert(std::pair("Authorization", token)); + m_headers.insert(std::pair("Authorization", "Bearer " + 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_event& callback) { - basic_request("/bots/" + std::to_string(bot_id), callback, [](const auto& j) { + basic_request(dpp::m_get, "/bots/" + bot_id.str(), callback, [](const auto& j) { return topgg::bot{j}; }); } @@ -108,7 +117,35 @@ topgg::async_result client::co_get_bot(const dpp::snowflake bot_id) } #endif -size_t client::get_server_count() { +void client::post_bot_commands(const topgg::post_bot_commands_completion_event& callback) { + if (m_legacy) { + throw new topgg::invalid_token{}; + } + + m_cluster.global_commands_get([this, callback](const dpp::confirmation_callback_t& discord_response) { + if (discord_response.is_error()) { + return callback(false); + } + + request(dpp::m_post, "/v1/projects/@me/commands", [callback](const auto& topgg_response) { + try { + internal_result::handle_response(topgg_response); + + callback(true); + } catch (TOPGG_UNUSED const std::exception& err) { + callback(false); + } + }, discord_response.http_info.body); + }); +} + +#ifdef DPP_CORO +dpp::async client::co_post_bot_commands() { + return dpp::async{[this](C&& cc) { return post_bot_commands(std::forward(cc)); }}; +} +#endif + +size_t client::get_bot_server_count() { #ifdef __TOPGG_TESTING__ return 2; #else @@ -122,35 +159,33 @@ size_t client::get_server_count() { #endif } -void client::post_server_count_inner(const size_t server_count, dpp::http_completion_event callback) { - auto headers{m_headers}; +void client::post_bot_server_count_inner(const size_t server_count, const dpp::http_completion_event callback) { 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); + request(dpp::m_post, "/bots/stats", callback, j.dump()); } -void client::post_server_count(const topgg::post_server_count_completion_event& callback) { - const auto server_count{get_server_count()}; +void client::post_bot_server_count(const topgg::post_bot_server_count_completion_event& callback) { + const auto server_count{get_bot_server_count()}; if (server_count == 0) { return callback(false); } - post_server_count_inner(server_count, [callback](const auto& response) { + post_bot_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_server_count() { - return dpp::async{[this](C&& cc) { return post_server_count(std::forward(cc)); }}; +dpp::async client::co_post_bot_server_count() { + return dpp::async{[this](C&& cc) { return post_bot_server_count(std::forward(cc)); }}; } #endif -void client::get_server_count(const topgg::get_server_count_completion_event& callback) { - basic_request>("/bots/stats", callback, [](const auto& j) { +void client::get_bot_server_count(const topgg::get_bot_server_count_completion_event& callback) { + basic_request>(dpp::m_get, "/bots/stats", callback, [](const auto& j) { std::optional server_count{}; try { @@ -162,8 +197,8 @@ void client::get_server_count(const topgg::get_server_count_completion_event& ca } #ifdef DPP_CORO -topgg::async_result> client::co_get_server_count() { - return topgg::async_result>{[this](C&& cc) { return get_server_count(std::forward(cc)); }}; +topgg::async_result> client::co_get_bot_server_count() { + return topgg::async_result>{[this](C&& cc) { return get_bot_server_count(std::forward(cc)); }}; } #endif @@ -172,7 +207,7 @@ void client::get_voters(size_t page, const topgg::get_voters_completion_event& c page = 1; } - basic_request>("/bots/" + m_id + "/votes?page=" + std::to_string(page), callback, [](const auto& j) { + basic_request>(dpp::m_get, "/bots/" + m_id.str() + "/votes?page=" + std::to_string(page), callback, [](const auto& j) { std::vector voters{}; for (const auto& part: j) { @@ -194,7 +229,7 @@ topgg::async_result> client::co_get_voters(size_t page #endif 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) { + basic_request(dpp::m_get, "/bots/check?userId=" + user_id.str(), callback, [](const auto& j) { return j["voted"].template get() != 0; }); } @@ -205,8 +240,36 @@ topgg::async_result client::co_has_voted(const dpp::snowflake user_id) { } #endif +void client::get_vote(const dpp::snowflake user_id, const char* user_source, const topgg::get_vote_completion_event& callback) { + if (m_legacy) { + throw new topgg::invalid_token{}; + } + + request(dpp::m_get, "/v1/projects/@me/votes/" + user_id.str() + "?source=" + user_source, [callback](const auto& response) { + if (response.status == 404) { + try { + const auto j{dpp::json::parse(response.body)}; + + if (j["title"].template get() == "Vote expired") { + return callback(result>{std::nullopt}); + } + } catch (TOPGG_UNUSED const std::exception&) {} + } + + callback(result>{response, [](const auto& j) { + return std::optional{topgg::vote{j}}; + }}); + }); +} + +#ifdef DPP_CORO +topgg::async_result> client::co_get_vote(const dpp::snowflake user_id, const char* user_source) { + return topgg::async_result>{[user_id, user_source, this](C&& cc) { return get_vote(user_id, user_source, std::forward(cc)); }}; +} +#endif + void client::is_weekend(const topgg::is_weekend_completion_event& callback) { - basic_request("/weekend", callback, [](const auto& j) { + basic_request(dpp::m_get, "/weekend", callback, [](const auto& j) { return j["is_weekend"].template get(); }); } @@ -217,22 +280,22 @@ topgg::async_result client::co_is_weekend() { } #endif -void client::start_autoposter(const topgg::autopost_completion_event& callback, time_t interval) { - if (interval < TOPGG_AUTOPOSTER_MIN_INTERVAL) { - interval = TOPGG_AUTOPOSTER_MIN_INTERVAL; +void client::start_bot_autoposter(const topgg::bot_autopost_completion_event& callback, time_t interval) { + if (interval < TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL) { + interval = TOPGG_BOT_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) { + if (!m_bot_autoposter_timer) { // clang-format off - m_autoposter_timer = m_cluster.start_timer([this, callback](TOPGG_UNUSED dpp::timer) { - const auto server_count{get_server_count()}; + m_bot_autoposter_timer = m_cluster.start_timer([this, callback](TOPGG_UNUSED dpp::timer) { + const auto server_count{get_bot_server_count()}; if (server_count > 0) { - post_server_count_inner(server_count, [callback, server_count](const auto& response) { + post_bot_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 { @@ -245,22 +308,22 @@ void client::start_autoposter(const topgg::autopost_completion_event& callback, } } -void client::start_autoposter(const time_t interval) { - start_autoposter([](TOPGG_UNUSED const auto&) {}, interval); +void client::start_bot_autoposter(const time_t interval) { + start_bot_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; +void client::start_bot_autoposter(topgg::bot_autoposter_source* source, const topgg::bot_autopost_completion_event& callback, time_t interval) { + if (!m_bot_autoposter_timer) { + if (interval < TOPGG_BOT_AUTOPOSTER_MIN_INTERVAL) { + interval = TOPGG_BOT_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)}; + m_bot_autoposter_timer = m_cluster.start_timer([this, callback, source](TOPGG_UNUSED dpp::timer) { + const auto server_count{source->get_bot_server_count(m_cluster)}; if (server_count > 0) { - post_server_count_inner(server_count, [callback, server_count](const auto& response) { + post_bot_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 { @@ -273,17 +336,17 @@ void client::start_autoposter(topgg::autoposter_source* source, const topgg::aut } } -void client::start_autoposter(topgg::autoposter_source* source, time_t interval) { - start_autoposter(source, [](TOPGG_UNUSED const auto&) {}, interval); +void client::start_bot_autoposter(topgg::bot_autoposter_source* source, time_t interval) { + start_bot_autoposter(source, [](TOPGG_UNUSED const auto&) {}, interval); } -void client::stop_autoposter() noexcept { - if (m_autoposter_timer) { - m_cluster.stop_timer(m_autoposter_timer); - m_autoposter_timer = 0; +void client::stop_bot_autoposter() noexcept { + if (m_bot_autoposter_timer) { + m_cluster.stop_timer(m_bot_autoposter_timer); + m_bot_autoposter_timer = 0; } } client::~client() { - stop_autoposter(); + stop_bot_autoposter(); } \ No newline at end of file diff --git a/src/models.cpp b/src/models.cpp index b44d8d3..24d4fda 100644 --- a/src/models.cpp +++ b/src/models.cpp @@ -1,5 +1,7 @@ #include +#include + #ifdef _WIN32 #include #include @@ -67,11 +69,9 @@ static void strptime(const char* s, const char* f, tm* t) { } \ }) -#define _TOPGG_SNOWFLAKE_FROM_JSON(j, name) \ - dpp::snowflake{j[#name].template get()} - using topgg::bot; using topgg::bot_query; +using topgg::vote; using topgg::voter; static time_t timestamp_from_id(const dpp::snowflake& id) { @@ -166,7 +166,7 @@ void bot_query::send(const topgg::get_bots_completion_event& callback) { path.pop_back(); - m_client->basic_request>(path, callback, [](const auto& j) { + m_client->basic_request>(dpp::m_get, path, callback, [](const auto& j) { std::vector bots{}; bots.reserve(j["count"].template get()); @@ -185,6 +185,34 @@ dpp::async> bot_query::co_send() { } #endif +static time_t parse_vote_time(const dpp::json& j, const char* key) { + auto j_text{j[key].template get()}; + tm text_tm{}; + + const auto dot_pos{j_text.find('.')}; + + if (dot_pos != std::string::npos) { + j_text = j_text.substr(0, dot_pos); + } + + strptime(j_text.data(), "%Y-%m-%dT%H:%M:%S", &text_tm); + + return mktime(&text_tm); +} + +vote::vote(const dpp::json& j) { + voted_at = parse_vote_time(j, "created_at"); + expires_at = parse_vote_time(j, "expires_at"); + + _TOPGG_DESERIALIZE(j, weight, size_t); +} + +bool vote::expired() const noexcept { + const auto now{std::chrono::system_clock::to_time_t(std::chrono::system_clock::now())}; + + return now >= expires_at; +} + voter::voter(const dpp::json& j) { id = _TOPGG_SNOWFLAKE_FROM_JSON(j, id); @@ -195,17 +223,17 @@ voter::voter(const dpp::json& j) { } 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); + return TOPGG_BASE_URL "/v1/widgets/large/" + std::string{ty} + "/" + id.str(); } 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); + return TOPGG_BASE_URL "/v1/widgets/small/votes/" + std::string{ty} + "/" + id.str(); } 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); + return TOPGG_BASE_URL "/v1/widgets/small/owner/" + std::string{ty} + "/" + id.str(); } 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); + return TOPGG_BASE_URL "/v1/widgets/small/social/" + std::string{ty} + "/" + id.str(); } \ No newline at end of file diff --git a/src/result.cpp b/src/result.cpp index 97d085a..d82bffb 100644 --- a/src/result.cpp +++ b/src/result.cpp @@ -60,26 +60,29 @@ using topgg::ratelimited; #pragma clang diagnostic pop #endif -void internal_result::prepare() const { - if (m_response.error != dpp::h_success) { - throw m_response.error; - } else if (m_response.status >= 400) { - switch (m_response.status) { - case 401: - throw invalid_token{}; - - case 404: - throw not_found{}; - - case 429: { - const auto j{json::parse(m_response.body)}; - const auto retry_after{j["retry_after"].template get()}; - - throw ratelimited{retry_after}; - } - - default: - throw internal_server_error{}; +void internal_result::handle_response(const dpp::http_request_completion_t& response) { + if (response.error != dpp::h_success) { + throw response.error; + } else if (response.status >= 400) { + switch (response.status) { + case 401: { + throw invalid_token{}; + } + + case 404: { + throw not_found{}; + } + + case 429: { + const auto j{json::parse(response.body)}; + const auto retry_after{j["retry_after"].template get()}; + + throw ratelimited{retry_after}; + } + + default: { + throw internal_server_error{}; + } } } } \ No newline at end of file diff --git a/src/webhooks/models.cpp b/src/webhooks/models.cpp index 06de76d..9124539 100644 --- a/src/webhooks/models.cpp +++ b/src/webhooks/models.cpp @@ -22,7 +22,7 @@ static std::unordered_map parse_query_string(const std return output; } -vote::vote(const json& j) { +vote_event::vote_event(const json& j) { #ifdef __TOPGG_DROGON_WEBHOOKS__ receiver_id = j["bot"].asString(); diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 4a162de..a5e92c8 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -14,11 +14,11 @@ 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_BOT_AUTOPOSTER "Build unit tests for testing Discord bot 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) +if(NOT TEST_API AND NOT TEST_BOT_AUTOPOSTER) set(ENABLE_API OFF) endif() @@ -56,8 +56,8 @@ if(TEST_API) add_test_file(test_api .) endif() -if(TEST_AUTOPOSTER) -add_test_file(test_autoposter .) +if(TEST_BOT_AUTOPOSTER) +add_test_file(test_bot_autoposter .) endif() if(ENABLE_CPP_HTTPLIB_WEBHOOKS) @@ -73,6 +73,6 @@ 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__) +target_compile_definitions(${NAME_WE} PUBLIC __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 index d52ffef..23a0ec5 100644 --- a/tests/cpp-httplib-webhooks/test_vote.cpp +++ b/tests/cpp-httplib-webhooks/test_vote.cpp @@ -7,13 +7,13 @@ template using cpp_httplib_webhook = topgg::webhook::cpp_httplib; -using topgg::webhook::vote; +using topgg::webhook::vote_event; -class my_vote_listener: public cpp_httplib_webhook { +class my_vote_listener: public cpp_httplib_webhook { public: - inline my_vote_listener(const std::string& authorization): cpp_httplib_webhook(authorization) {} + inline my_vote_listener(const std::string& authorization): cpp_httplib_webhook(authorization) {} - void callback(const vote& v) override { + void callback(const vote_event& v) override { std::cout << "A user with the ID of " << v.voter_id << " has voted us on Top.gg!" << std::endl; } }; diff --git a/tests/drogon-webhooks/test_vote.cpp b/tests/drogon-webhooks/test_vote.cpp index 600bf5c..ee6efd0 100644 --- a/tests/drogon-webhooks/test_vote.cpp +++ b/tests/drogon-webhooks/test_vote.cpp @@ -8,11 +8,11 @@ template using drogon_webhook = topgg::webhook::drogon; -using topgg::webhook::vote; +using topgg::webhook::vote_event; -class my_vote_listener: public ::drogon::HttpSimpleController, public drogon_webhook { +class my_vote_listener: public ::drogon::HttpSimpleController, public drogon_webhook { public: - inline my_vote_listener(const std::string& authorization): drogon_webhook(authorization) {} + inline my_vote_listener(const std::string& authorization): drogon_webhook(authorization) {} TOPGG_DROGON_WEBHOOK(); @@ -20,7 +20,7 @@ class my_vote_listener: public ::drogon::HttpSimpleController