From 458ebb4c77d50528373559bca625c89c49f8ea4f Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Fri, 20 Oct 2023 10:54:29 +0100 Subject: [PATCH 01/14] docs: added a note to setting_status for guild cache (#955) --- docpages/example_programs/misc/setting_status.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docpages/example_programs/misc/setting_status.md b/docpages/example_programs/misc/setting_status.md index a487ec8a54..189eed85d3 100644 --- a/docpages/example_programs/misc/setting_status.md +++ b/docpages/example_programs/misc/setting_status.md @@ -2,6 +2,8 @@ A bot status is pretty cool, and it'd be cooler if you knew how to do it! This tutorial will cover how to set the bot status to say `Playing games!`, as well as covering how to set the status to the amount of guilds every two minutes. +\note dpp::get_guild_cache requires the bot to have the guild cache enabled, if your bot has this disabled then you can't use that. Instead, you should look to use dpp::cluster::current_application_get and get the `approximate_guild_count` from dpp::application in the callback. + First, we'll cover setting the bot status to `Playing games!`. \include{cpp} setting_status1.cpp From f9c55bf23770a53341fda1648ce4a6624ce77a51 Mon Sep 17 00:00:00 2001 From: Eric <52634785+erics118@users.noreply.github.com> Date: Fri, 20 Oct 2023 19:29:43 -0400 Subject: [PATCH 02/14] fix: make embed color optional (#961) --- include/dpp/message.h | 2 +- src/dpp/message.cpp | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/include/dpp/message.h b/include/dpp/message.h index 00e04f7bfc..f877bb2db6 100644 --- a/include/dpp/message.h +++ b/include/dpp/message.h @@ -658,7 +658,7 @@ struct DPP_EXPORT embed { /** Optional: timestamp of embed content */ time_t timestamp; /** Optional: color code of the embed */ - uint32_t color; + std::optional color; /** Optional: footer information */ std::optional footer; /** Optional: image information */ diff --git a/src/dpp/message.cpp b/src/dpp/message.cpp index 810b76f2a2..c3218c8fe3 100644 --- a/src/dpp/message.cpp +++ b/src/dpp/message.cpp @@ -487,7 +487,7 @@ component &component::add_default_value(const snowflake id, const component_defa embed::~embed() = default; -embed::embed() : timestamp(0), color(0) { +embed::embed() : timestamp(0) { } message::message() : managed(0), channel_id(0), guild_id(0), sent(0), edited(0), webhook_id(0), @@ -618,7 +618,9 @@ embed::embed(json* j) : embed() { description = string_not_null(j, "description"); url = string_not_null(j, "url"); timestamp = ts_not_null(j, "timestamp"); - color = int32_not_null(j, "color"); + if (j->contains("color")) { + color = int32_not_null(j, "color"); + } if (j->contains("footer")) { dpp::embed_footer f; json& fj = (*j)["footer"]; @@ -962,7 +964,9 @@ json message::to_json(bool with_id, bool is_interaction_response) const { if (!embed.url.empty()) { e["url"] = embed.url; } - e["color"] = embed.color; + if (embed.color.has_value()) { + e["color"] = embed.color.value(); + } if (embed.footer.has_value()) { e["footer"]["text"] = embed.footer->text; e["footer"]["icon_url"] = embed.footer->icon_url; From 359ce611881c62520d22677f8a5d7a41c2c4a18b Mon Sep 17 00:00:00 2001 From: professor <72993616+professor91@users.noreply.github.com> Date: Sat, 21 Oct 2023 18:33:26 +0530 Subject: [PATCH 03/14] refactor: cleaned up Unit Tests (#948) --- src/unittest/cache.cpp | 64 + src/unittest/discord_objects.cpp | 567 ++++++++ src/unittest/errors.cpp | 118 ++ src/unittest/gateway_events.cpp | 1296 ++++++++++++++++++ src/unittest/http.cpp | 93 ++ src/unittest/test.cpp | 2199 +----------------------------- src/unittest/test.h | 30 + src/unittest/utilities.cpp | 190 +++ 8 files changed, 2365 insertions(+), 2192 deletions(-) create mode 100644 src/unittest/cache.cpp create mode 100644 src/unittest/discord_objects.cpp create mode 100644 src/unittest/errors.cpp create mode 100644 src/unittest/gateway_events.cpp create mode 100644 src/unittest/http.cpp create mode 100644 src/unittest/utilities.cpp diff --git a/src/unittest/cache.cpp b/src/unittest/cache.cpp new file mode 100644 index 0000000000..6892e090cb --- /dev/null +++ b/src/unittest/cache.cpp @@ -0,0 +1,64 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include "test.h" + +#include + +/* Unit tests for Cache */ +void cache_tests(dpp::cluster& bot) { + set_test(USER_GET_CACHED_PRESENT, false); + try { + dpp::user_identified u = bot.user_get_cached_sync(TEST_USER_ID); + set_test(USER_GET_CACHED_PRESENT, (u.id == TEST_USER_ID)); + } + catch (const std::exception&) { + set_test(USER_GET_CACHED_PRESENT, false); + } + + set_test(USER_GET_CACHED_ABSENT, false); + try { + /* This is the snowflake ID of a discord staff member. + * We assume here that staffer's discord IDs will remain constant + * for long periods of time and they won't lurk in the unit test server. + * If this becomes not true any more, we'll pick another well known + * user ID. + */ + dpp::user_identified u = bot.user_get_cached_sync(90339695967350784); + set_test(USER_GET_CACHED_ABSENT, (u.id == dpp::snowflake(90339695967350784))); + } + catch (const std::exception&) { + set_test(USER_GET_CACHED_ABSENT, false); + } + + set_test(CUSTOMCACHE, false); + dpp::cache testcache; + test_cached_object_t* tco = new test_cached_object_t(666); + tco->foo = "bar"; + testcache.store(tco); + test_cached_object_t* found_tco = testcache.find(666); + if (found_tco && found_tco->id == dpp::snowflake(666) && found_tco->foo == "bar") { + set_test(CUSTOMCACHE, true); + } else { + set_test(CUSTOMCACHE, false); + } + testcache.remove(found_tco); +} diff --git a/src/unittest/discord_objects.cpp b/src/unittest/discord_objects.cpp new file mode 100644 index 0000000000..0bd1ff9eae --- /dev/null +++ b/src/unittest/discord_objects.cpp @@ -0,0 +1,567 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include "test.h" + +/* Unit tests for Discord objects (webhook, interaction, user etc.) */ +void discord_objects_tests() { + // test webhook + set_test(WEBHOOK, false); + try { + dpp::webhook test_wh("https://discord.com/api/webhooks/833047646548133537/ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE"); + set_test(WEBHOOK, (test_wh.token == "ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE") && (test_wh.id == dpp::snowflake(833047646548133537))); + } + catch (const dpp::exception&) { + set_test(WEBHOOK, false); + } + + { // test dpp::snowflake + start_test(SNOWFLAKE); + bool success = true; + dpp::snowflake s = 69420; + json j; + j["value"] = s; + success = dpp::snowflake_not_null(&j, "value") == 69420 && success; + DPP_CHECK_CONSTRUCT_ASSIGN(SNOWFLAKE, dpp::snowflake, success); + s = 42069; + success = success && (s == 42069 && s == dpp::snowflake{42069} && s == "42069"); + success = success && (dpp::snowflake{69} < dpp::snowflake{420} && (dpp::snowflake{69} < 420)); + s = "69420"; + success = success && s == 69420; + auto conversion_test = [](dpp::snowflake sl) { + return sl.str(); + }; + s = conversion_test(std::string{"1337"}); + success = success && s == 1337; /* THIS BREAKS (and i do not care very much): && s == conversion_test(dpp::snowflake{"1337"}); */ + success = success && dpp::snowflake{0} == 0; + set_test(SNOWFLAKE, success); + } + + { // test interaction_create_t::get_parameter + // create a fake interaction + dpp::cluster cluster(""); + dpp::discord_client client(&cluster, 1, 1, ""); + dpp::interaction_create_t interaction(&client, ""); + + /* Check the method with subcommands */ + set_test(GET_PARAMETER_WITH_SUBCOMMANDS, false); + + dpp::command_interaction cmd_data; // command + cmd_data.type = dpp::ctxm_chat_input; + cmd_data.name = "command"; + + dpp::command_data_option subcommandgroup; // subcommand group + subcommandgroup.name = "group"; + subcommandgroup.type = dpp::co_sub_command_group; + + dpp::command_data_option subcommand; // subcommand + subcommand.name = "add"; + subcommand.type = dpp::co_sub_command; + + dpp::command_data_option option1; // slashcommand option + option1.name = "user"; + option1.type = dpp::co_user; + option1.value = dpp::snowflake(189759562910400512); + + dpp::command_data_option option2; // slashcommand option + option2.name = "checked"; + option2.type = dpp::co_boolean; + option2.value = true; + + // add them + subcommand.options.push_back(option1); + subcommand.options.push_back(option2); + subcommandgroup.options.push_back(subcommand); + cmd_data.options.push_back(subcommandgroup); + interaction.command.data = cmd_data; + + dpp::snowflake value1 = std::get(interaction.get_parameter("user")); + set_test(GET_PARAMETER_WITH_SUBCOMMANDS, value1 == dpp::snowflake(189759562910400512)); + + /* Check the method without subcommands */ + set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, false); + + dpp::command_interaction cmd_data2; // command + cmd_data2.type = dpp::ctxm_chat_input; + cmd_data2.name = "command"; + + dpp::command_data_option option3; // slashcommand option + option3.name = "number"; + option3.type = dpp::co_integer; + option3.value = int64_t(123456); + + cmd_data2.options.push_back(option3); + interaction.command.data = cmd_data2; + + int64_t value2 = std::get(interaction.get_parameter("number")); + set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, value2 == 123456); + } + + { // test dpp::command_option_choice::fill_from_json + set_test(OPTCHOICE_DOUBLE, false); + set_test(OPTCHOICE_INT, false); + set_test(OPTCHOICE_BOOL, false); + set_test(OPTCHOICE_SNOWFLAKE, false); + set_test(OPTCHOICE_STRING, false); + json j; + dpp::command_option_choice choice; + j["value"] = 54.321; + choice.fill_from_json(&j); + bool success_double = std::holds_alternative(choice.value); + j["value"] = 8223372036854775807; + choice.fill_from_json(&j); + bool success_int = std::holds_alternative(choice.value); + j["value"] = -8223372036854775807; + choice.fill_from_json(&j); + bool success_int2 = std::holds_alternative(choice.value); + j["value"] = true; + choice.fill_from_json(&j); + bool success_bool = std::holds_alternative(choice.value); + dpp::snowflake s(845266178036516757); // example snowflake + j["value"] = s; + choice.fill_from_json(&j); + bool success_snowflake = std::holds_alternative(choice.value) && std::get(choice.value) == s; + j["value"] = "foobar"; + choice.fill_from_json(&j); + bool success_string = std::holds_alternative(choice.value); + set_test(OPTCHOICE_DOUBLE, success_double); + set_test(OPTCHOICE_INT, success_int && success_int2); + set_test(OPTCHOICE_BOOL, success_bool); + set_test(OPTCHOICE_SNOWFLAKE, success_snowflake); + set_test(OPTCHOICE_STRING, success_string); + } + + { // test permissions + set_test(PERMISSION_CLASS, false); + bool success = false; + auto p = dpp::permission(); + p = 16; + success = p == 16; + p |= 4; + success = p == 20 && success; + p <<= 8; // left shift + success = p == 5120 && success; + auto s = std::to_string(p); + success = s == "5120" && success; + p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_administrator); + success = !p.has(dpp::p_administrator, dpp::p_ban_members) && success; // must return false because they're not both set + success = !p.has(dpp::p_administrator | dpp::p_ban_members) && success; + success = p.can(dpp::p_ban_members) && success; + success = p.can(dpp::p_speak) && success; + + constexpr auto permission_test = [](dpp::permission p) constexpr noexcept { + bool success{true}; + + p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_connect); + p.set(dpp::p_administrator, dpp::p_ban_members); + success = p.has(dpp::p_administrator) && success; + success = p.has(dpp::p_administrator) && p.has(dpp::p_ban_members) && success; + success = p.has(dpp::p_administrator, dpp::p_ban_members) && success; + success = p.has(dpp::p_administrator | dpp::p_ban_members) && success; + success = p.add(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; + success = !p.remove(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; + p.remove(dpp::p_administrator); + success = p.can(dpp::p_ban_members) && success; + success = !p.can(dpp::p_speak, dpp::p_ban_members) && success; + success = p.can_any(dpp::p_speak, dpp::p_ban_members) && success; + return success; + }; + constexpr auto constexpr_success = permission_test({~uint64_t{0}}); // test in constant evaluated + success = permission_test({~uint64_t{0}}) && constexpr_success && success; // test at runtime + set_test(PERMISSION_CLASS, success); + } + + { // some dpp::user methods + dpp::user user1; + user1.id = 189759562910400512; + user1.discriminator = 0001; + user1.username = "brain"; + + set_test(USER_GET_MENTION, false); + set_test(USER_GET_MENTION, user1.get_mention() == "<@189759562910400512>"); + + set_test(USER_FORMAT_USERNAME, false); + set_test(USER_FORMAT_USERNAME, user1.format_username() == "brain#0001"); + + set_test(USER_GET_CREATION_TIME, false); + set_test(USER_GET_CREATION_TIME, (uint64_t)user1.get_creation_time() == 1465312605); + + set_test(USER_GET_URL, false); + + dpp::user user2; + set_test(USER_GET_URL, + user1.get_url() == dpp::utility::url_host + "/users/189759562910400512" && + user2.get_url() == "" + ); + } + + { // avatar size function + set_test(UTILITY_AVATAR_SIZE, false); + bool success = false; + success = dpp::utility::avatar_size(0).empty(); + success = dpp::utility::avatar_size(16) == "?size=16" && success; + success = dpp::utility::avatar_size(256) == "?size=256" && success; + success = dpp::utility::avatar_size(4096) == "?size=4096" && success; + success = dpp::utility::avatar_size(8192).empty() && success; + success = dpp::utility::avatar_size(3000).empty() && success; + set_test(UTILITY_AVATAR_SIZE, success); + } + + // some dpp::role test + set_test(ROLE_COMPARE, false); + dpp::role role_1, role_2; + role_1.position = 1; + role_2.position = 2; + set_test(ROLE_COMPARE, role_1 < role_2 && role_1 != role_2); + + { // message methods + dpp::message m; + m.guild_id = 825407338755653642; + m.channel_id = 956230231277072415; + m.id = 1151617986541666386; + + dpp::message m2; + m2.guild_id = 825407338755653642; + m2.channel_id = 956230231277072415; + + dpp::message m3; + m3.guild_id = 825407338755653642; + m3.id = 1151617986541666386; + + dpp::message m4; + m4.channel_id = 956230231277072415; + m4.id = 1151617986541666386; + + dpp::message m5; + m5.guild_id = 825407338755653642; + + dpp::message m6; + m6.channel_id = 956230231277072415; + + dpp::message m7; + m7.id = 1151617986541666386; + + dpp::message m8; + + set_test(MESSAGE_GET_URL, false); + set_test(MESSAGE_GET_URL, + m.get_url() == dpp::utility::url_host + "/channels/825407338755653642/956230231277072415/1151617986541666386" && + m2.get_url() == "" && + m3.get_url() == "" && + m4.get_url() == "" && + m5.get_url() == "" && + m6.get_url() == "" && + m7.get_url() == "" && + m8.get_url() == "" + ); + } + + { // channel methods + set_test(CHANNEL_SET_TYPE, false); + dpp::channel c; + c.set_flags(dpp::c_nsfw | dpp::c_video_quality_720p); + c.set_type(dpp::CHANNEL_CATEGORY); + bool before = c.is_category() && !c.is_forum(); + c.set_type(dpp::CHANNEL_FORUM); + bool after = !c.is_category() && c.is_forum(); + set_test(CHANNEL_SET_TYPE, before && after); + + set_test(CHANNEL_GET_MENTION, false); + c.id = 825411707521728511; + set_test(CHANNEL_GET_MENTION, c.get_mention() == "<#825411707521728511>"); + + set_test(CHANNEL_GET_URL, false); + c.guild_id = 825407338755653642; + + dpp::channel c2; + c2.id = 825411707521728511; + + dpp::channel c3; + c3.guild_id = 825407338755653642; + + dpp::channel c4; + + set_test(CHANNEL_GET_URL, + c.get_url() == dpp::utility::url_host + "/channels/825407338755653642/825411707521728511" && + c2.get_url() == "" && + c3.get_url() == "" && + c4.get_url() == "" + ); + } + + { // cdn endpoint url getter + set_test(UTILITY_CDN_ENDPOINT_URL_HASH, false); + bool success = false; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_jpg, 0).empty(); + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 0) == "https://cdn.discordapp.com/foobar/test.png" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 128) == "https://cdn.discordapp.com/foobar/test.png?size=128" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_gif, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.png" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, false) == "https://cdn.discordapp.com/foobar/test/12345.png" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test.gif" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_gif, 0, false, false).empty() && success; + set_test(UTILITY_CDN_ENDPOINT_URL_HASH, success); + } + + { // user url getter + dpp::user user1; + user1.id = 189759562910400512; + user1.username = "Brain"; + user1.discriminator = 0001; + + auto user2 = user1; + user2.avatar = "5532c6414c70765a28cf9448c117205f"; + + auto user3 = user2; + user3.flags |= dpp::u_animated_icon; + + set_test(USER_GET_AVATAR_URL, false); + set_test(USER_GET_AVATAR_URL, + dpp::user().get_avatar_url().empty() && + user1.get_avatar_url() == dpp::utility::cdn_host + "/embed/avatars/1.png" && + user2.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.png" && + user2.get_avatar_url(0, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.webp" && + user2.get_avatar_url(0, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.jpg" && + user3.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" && + user3.get_avatar_url(4096, dpp::i_gif) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=4096" && + user3.get_avatar_url(512, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && + user3.get_avatar_url(512, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && + user3.get_avatar_url(16, dpp::i_jpg, false) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.jpg?size=16" && + user3.get_avatar_url(5000) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" + ); + } + + { // sticker url getter + set_test(STICKER_GET_URL, false); + dpp::sticker s; + s.format_type = dpp::sf_png; + bool success = s.get_url().empty(); + s.id = 12345; + success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.png" && success; + s.format_type = dpp::sf_gif; + success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.gif" && success; + s.format_type = dpp::sf_lottie; + success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.json" && success; + set_test(STICKER_GET_URL, success); + } + + { // emoji url getter + dpp::emoji emoji; + emoji.id = 825407338755653641; + + set_test(EMOJI_GET_URL, false); + set_test(EMOJI_GET_URL, emoji.get_url() == dpp::utility::cdn_host + "/emojis/825407338755653641.png"); + } + + { // utility methods + set_test(UTILITY_GUILD_NAVIGATION, false); + auto gn1 = dpp::utility::guild_navigation(123, dpp::utility::gnt_customize); + auto gn2 = dpp::utility::guild_navigation(1234, dpp::utility::gnt_browse); + auto gn3 = dpp::utility::guild_navigation(12345, dpp::utility::gnt_guide); + set_test(UTILITY_GUILD_NAVIGATION, gn1 == "<123:customize>" && gn2 == "<1234:browse>" && gn3 == "<12345:guide>"); + + set_test(UTILITY_ICONHASH, false); + auto iconhash1 = dpp::utility::iconhash("a_5532c6414c70765a28cf9448c117205f"); + set_test(UTILITY_ICONHASH, iconhash1.first == 6139187225817019994 && + iconhash1.second == 2940732121894297695 && + iconhash1.to_string() == "5532c6414c70765a28cf9448c117205f" + ); + + set_test(UTILITY_MAKE_URL_PARAMETERS, false); + auto url_params1 = dpp::utility::make_url_parameters({ + {"foo", 15}, + {"bar", 7} + }); + auto url_params2 = dpp::utility::make_url_parameters({ + {"foo", "hello"}, + {"bar", "two words"} + }); + set_test(UTILITY_MAKE_URL_PARAMETERS, url_params1 == "?bar=7&foo=15" && url_params2 == "?bar=two%20words&foo=hello"); + + set_test(UTILITY_MARKDOWN_ESCAPE, false); + auto escaped = dpp::utility::markdown_escape( + "> this is a quote\n" + "**some bold text**"); + set_test(UTILITY_MARKDOWN_ESCAPE, "\\>this is a quote\\n\\*\\*some bold text\\*\\*"); + + set_test(UTILITY_TOKENIZE, false); + auto tokens = dpp::utility::tokenize("some Whitespace seperated Text to Tokenize", " "); + std::vector expected_tokens = {"some", "Whitespace", "seperated", "Text", "to", "Tokenize"}; + set_test(UTILITY_TOKENIZE, tokens == expected_tokens); + + set_test(UTILITY_URL_ENCODE, false); + auto url_encoded = dpp::utility::url_encode("S2-^$1Nd+U!g'8+_??o?p-bla bla"); + set_test(UTILITY_URL_ENCODE, url_encoded == "S2-%5E%241Nd%2BU%21g%278%2B_%3F%3Fo%3Fp-bla%20bla"); + + set_test(UTILITY_SLASHCOMMAND_MENTION, false); + auto mention1 = dpp::utility::slashcommand_mention(123, "name"); + auto mention2 = dpp::utility::slashcommand_mention(123, "name", "sub"); + auto mention3 = dpp::utility::slashcommand_mention(123, "name", "group", "sub"); + bool success = mention1 == "" && mention2 == "" && mention3 == ""; + set_test(UTILITY_SLASHCOMMAND_MENTION, success); + + set_test(UTILITY_CHANNEL_MENTION, false); + auto channel_mention = dpp::utility::channel_mention(123); + set_test(UTILITY_CHANNEL_MENTION, channel_mention == "<#123>"); + + set_test(UTILITY_USER_MENTION, false); + auto user_mention = dpp::utility::user_mention(123); + set_test(UTILITY_USER_MENTION, user_mention == "<@123>"); + + set_test(UTILITY_ROLE_MENTION, false); + auto role_mention = dpp::utility::role_mention(123); + set_test(UTILITY_ROLE_MENTION, role_mention == "<@&123>"); + + set_test(UTILITY_EMOJI_MENTION, false); + auto emoji_mention1 = dpp::utility::emoji_mention("role1", 123, false); + auto emoji_mention2 = dpp::utility::emoji_mention("role2", 234, true); + auto emoji_mention3 = dpp::utility::emoji_mention("white_check_mark", 0, false); + auto emoji_mention4 = dpp::utility::emoji_mention("white_check_mark", 0, true); + set_test(UTILITY_EMOJI_MENTION, + emoji_mention1 == "<:role1:123>" && + emoji_mention2 == "" && + emoji_mention3 == ":white_check_mark:" && + emoji_mention4 == ":white_check_mark:" + ); + + set_test(UTILITY_USER_URL, false); + auto user_url = dpp::utility::user_url(123); + set_test(UTILITY_USER_URL, + user_url == dpp::utility::url_host + "/users/123" && + dpp::utility::user_url(0) == "" + ); + + set_test(UTILITY_MESSAGE_URL, false); + auto message_url = dpp::utility::message_url(1,2,3); + set_test(UTILITY_MESSAGE_URL, + message_url == dpp::utility::url_host+ "/channels/1/2/3" && + dpp::utility::message_url(0,2,3) == "" && + dpp::utility::message_url(1,0,3) == "" && + dpp::utility::message_url(1,2,0) == "" && + dpp::utility::message_url(0,0,3) == "" && + dpp::utility::message_url(0,2,0) == "" && + dpp::utility::message_url(1,0,0) == "" && + dpp::utility::message_url(0,0,0) == "" + ); + + set_test(UTILITY_CHANNEL_URL, false); + auto channel_url = dpp::utility::channel_url(1,2); + set_test(UTILITY_CHANNEL_URL, + channel_url == dpp::utility::url_host+ "/channels/1/2" && + dpp::utility::channel_url(0,2) == "" && + dpp::utility::channel_url(1,0) == "" && + dpp::utility::channel_url(0,0) == "" + ); + + set_test(UTILITY_THREAD_URL, false); + auto thread_url = dpp::utility::thread_url(1,2); + set_test(UTILITY_THREAD_URL, + thread_url == dpp::utility::url_host+ "/channels/1/2" && + dpp::utility::thread_url(0,2) == "" && + dpp::utility::thread_url(1,0) == "" && + dpp::utility::thread_url(0,0) == "" + ); + } + + { // dpp event classes + start_test(EVENT_CLASS); + bool success = true; + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::log_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_execute_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_state_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::interaction_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::slashcommand_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::button_click_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::form_submit_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::autocomplete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::context_menu_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_context_menu_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_context_menu_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::select_click_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_stickers_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_join_request_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::ready_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::resumed_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::typing_start_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_track_marker_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_members_chunk_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_emoji_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_bulk_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_pins_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_all_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_server_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_emojis_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::presence_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::webhooks_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_integrations_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_audit_log_entry_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_list_sync_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_member_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_members_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_buffer_send_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_user_talking_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_ready_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_receive_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_speaking_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_disconnect_t, success); + set_test(EVENT_CLASS, success); + } +} diff --git a/src/unittest/errors.cpp b/src/unittest/errors.cpp new file mode 100644 index 0000000000..8584777eaf --- /dev/null +++ b/src/unittest/errors.cpp @@ -0,0 +1,118 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include "test.h" + +/* Unit tests for human-readable error translation */ +void errors_test() { + set_test(ERRORS, false); + + /* Prepare a confirmation_callback_t in error state (400) */ + dpp::confirmation_callback_t error_test; + bool error_message_success = false; + error_test.http_info.status = 400; + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"options\": {\ + \"0\": {\ + \"name\": {\ + \"_errors\": [\ + {\ + \"code\": \"STRING_TYPE_REGEX\",\ + \"message\": \"String value did not match validation regex.\"\ + },\ + {\ + \"code\": \"APPLICATION_COMMAND_INVALID_NAME\",\ + \"message\": \"Command name is invalid\"\ + }\ + ]\ + }\ + }\ + }\ + }\ + }"; + error_message_success = (error_test.get_error().human_readable == "50035: Invalid Form Body\n\t- options[0].name: String value did not match validation regex. (STRING_TYPE_REGEX)\n\t- options[0].name: Command name is invalid (APPLICATION_COMMAND_INVALID_NAME)"); + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"type\": {\ + \"_errors\": [\ + {\ + \"code\": \"BASE_TYPE_CHOICES\",\ + \"message\": \"Value must be one of {4, 5, 9, 10, 11}.\"\ + }\ + ]\ + }\ + }\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - type: Value must be one of {4, 5, 9, 10, 11}. (BASE_TYPE_CHOICES)"); + + error_test.http_info.body = "{\ + \"message\": \"Unknown Guild\",\ + \"code\": 10004\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "10004: Unknown Guild"); + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"allowed_mentions\": {\ + \"_errors\": [\ + {\ + \"code\": \"MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE\",\ + \"message\": \"parse:[\\\"users\\\"] and users: [ids...] are mutually exclusive.\"\ + }\ + ]\ + }\ + }\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - allowed_mentions: parse:[\"users\"] and users: [ids...] are mutually exclusive. (MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE)"); + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"1\": {\ + \"options\": {\ + \"1\": {\ + \"description\": {\ + \"_errors\": [\ + {\ + \"code\": \"BASE_TYPE_BAD_LENGTH\",\ + \"message\": \"Must be between 1 and 100 in length.\"\ + }\ + ]\ + }\ + }\ + }\ + }\ + }\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - [1].options[1].description: Must be between 1 and 100 in length. (BASE_TYPE_BAD_LENGTH)"); + + set_test(ERRORS, error_message_success); +} diff --git a/src/unittest/gateway_events.cpp b/src/unittest/gateway_events.cpp new file mode 100644 index 0000000000..beddfbe89f --- /dev/null +++ b/src/unittest/gateway_events.cpp @@ -0,0 +1,1296 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include "test.h" + +#include +#include + +/* Unit tests for Gateway events */ +void gateway_events_tests(const std::string& token, dpp::cluster& bot) { + std::vector test_image = load_test_image(); + std::vector testaudio = load_test_audio(); + + set_test(PRESENCE, false); + set_test(CLUSTER, false); + try { + bot.set_websocket_protocol(dpp::ws_etf); + set_test(CLUSTER, true); + set_test(CONNECTION, false); + set_test(GUILDCREATE, false); + set_test(ICONHASH, false); + + set_test(MSGCOLLECT, false); + if (!offline) { + /* Intentional leak: freed on unit test end */ + [[maybe_unused]] + message_collector* collect_messages = new message_collector(&bot, 25); + } + + set_test(JSON_PARSE_ERROR, false); + dpp::rest_request(&bot, "/nonexistent", "address", "", dpp::m_get, "", [](const dpp::confirmation_callback_t& e) { + if (e.is_error() && e.get_error().code == 404) { + set_test(JSON_PARSE_ERROR, true); + } else { + set_test(JSON_PARSE_ERROR, false); + } + }); + + dpp::utility::iconhash i; + std::string dummyval("fcffffffffffff55acaaaaaaaaaaaa66"); + i = dummyval; + set_test(ICONHASH, (i.to_string() == dummyval)); + + /* This ensures we test both protocols, as voice is json and shard is etf */ + bot.set_websocket_protocol(dpp::ws_etf); + + bot.on_form_submit([&](const dpp::form_submit_t & event) { + }); + + /* This is near impossible to test without a 'clean room' voice channel. + * We attach this event just so that the decoder events are fired while we + * are sending audio later, this way if the audio receive code is plain unstable + * the test suite will crash and fail. + */ + bot.on_voice_receive_combined([&](const auto& event) { + }); + + std::promise ready_promise; + std::future ready_future = ready_promise.get_future(); + bot.on_ready([&](const dpp::ready_t & event) { + set_test(CONNECTION, true); + ready_promise.set_value(); + + set_test(APPCOMMAND, false); + set_test(LOGGER, false); + bot.log(dpp::ll_info, "Test log message"); + + bot.guild_command_create(dpp::slashcommand().set_name("testcommand") + .set_description("Test command for DPP unit test") + .add_option(dpp::command_option(dpp::co_attachment, "file", "a file")) + .set_application_id(bot.me.id) + .add_localization("fr", "zut", "Ou est la salor dans Discord?"), + TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(APPCOMMAND, true); + set_test(DELCOMMAND, false); + dpp::slashcommand s = std::get(callback.value); + bot.guild_command_delete(s.id, TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + dpp::message test_message(TEST_TEXT_CHANNEL_ID, "test message"); + + set_test(DELCOMMAND, true); + set_test(MESSAGECREATE, false); + set_test(MESSAGEEDIT, false); + set_test(MESSAGERECEIVE, false); + test_message.add_file("no-mime", "test"); + test_message.add_file("test.txt", "test", "text/plain"); + test_message.add_file("test.png", std::string{test_image.begin(), test_image.end()}, "image/png"); + bot.message_create(test_message, [&bot](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(MESSAGECREATE, true); + set_test(REACT, false); + dpp::message m = std::get(callback.value); + set_test(REACTEVENT, false); + bot.message_add_reaction(m.id, TEST_TEXT_CHANNEL_ID, "😄", [](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(REACT, true); + } else { + set_test(REACT, false); + } + }); + set_test(EDITEVENT, false); + bot.message_edit(dpp::message(m).set_content("test edit"), [](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(MESSAGEEDIT, true); + } + }); + } + }); + } else { + set_test(DELCOMMAND, false); + } + }); + } + }); + }); + + std::mutex loglock; + bot.on_log([&](const dpp::log_t & event) { + std::lock_guard locker(loglock); + if (event.severity > dpp::ll_trace) { + std::cout << "[" << std::fixed << std::setprecision(3) << (dpp::utility::time_f() - get_start_time()) << "]: [\u001b[36m" << dpp::utility::loglevel(event.severity) << "\u001b[0m] " << event.message << "\n"; + } + if (event.message == "Test log message") { + set_test(LOGGER, true); + } + }); + + set_test(RUNONCE, false); + uint8_t runs = 0; + for (int x = 0; x < 10; ++x) { + if (dpp::run_once()) { + runs++; + } + } + set_test(RUNONCE, (runs == 1)); + + bot.on_voice_ready([&](const dpp::voice_ready_t & event) { + set_test(VOICECONN, true); + dpp::discord_voice_client* v = event.voice_client; + set_test(VOICESEND, false); + if (v && v->is_ready()) { + v->send_audio_raw(reinterpret_cast(testaudio.data()), testaudio.size()); // TODO: fix type punning + } else { + set_test(VOICESEND, false); + } + }); + + bot.on_invite_create([](const dpp::invite_create_t &event) { + auto &inv = event.created_invite; + if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID && inv.created_at != 0 && inv.max_uses == 100) { + set_test(INVITE_CREATE_EVENT, true); + } + }); + + bot.on_invite_delete([](const dpp::invite_delete_t &event) { + auto &inv = event.deleted_invite; + if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID) { + set_test(INVITE_DELETE_EVENT, true); + } + }); + + bot.on_voice_buffer_send([&](const dpp::voice_buffer_send_t & event) { + if (event.buffer_size == 0) { + set_test(VOICESEND, true); + } + }); + + set_test(SYNC, false); + if (!offline) { + dpp::message m = dpp::sync(&bot, &dpp::cluster::message_create, dpp::message(TEST_TEXT_CHANNEL_ID, "TEST")); + set_test(SYNC, m.content == "TEST"); + } + + bot.on_guild_create([&](const dpp::guild_create_t & event) { + if (event.created->id == TEST_GUILD_ID) { + set_test(GUILDCREATE, true); + if (event.presences.size() && event.presences.begin()->second.user_id > 0) { + set_test(PRESENCE, true); + } + dpp::guild* g = dpp::find_guild(TEST_GUILD_ID); + set_test(CACHE, false); + if (g) { + set_test(CACHE, true); + set_test(VOICECONN, false); + dpp::discord_client* s = bot.get_shard(0); + s->connect_voice(g->id, TEST_VC_ID, false, false); + } + else { + set_test(CACHE, false); + } + } + }); + + // this helper class contains logic for the message tests, deletes the message when all tests are done + class message_test_helper { + private: + std::mutex mutex; + bool pin_tested = false; + bool thread_tested = false; + std::array files_tested{}; + std::array files_success{}; + dpp::snowflake channel_id; + dpp::snowflake message_id; + dpp::cluster ⊥ + + void delete_message_if_done() { + if (files_tested == std::array{true, true, true} && pin_tested && thread_tested) { + set_test(MESSAGEDELETE, false); + bot.message_delete(message_id, channel_id, [](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(MESSAGEDELETE, true); + } + }); + } + } + + void set_pin_tested() { + assert(!pin_tested); + pin_tested = true; + delete_message_if_done(); + } + + void set_thread_tested() { + assert(!thread_tested); + thread_tested = true; + delete_message_if_done(); + } + + void set_file_tested(size_t index) { + assert(!files_tested[index]); + files_tested[index] = true; + if (files_tested == std::array{true, true, true}) { + set_test(MESSAGEFILE, files_success == std::array{true, true, true}); + } + delete_message_if_done(); + } + + void test_threads(const dpp::message &message) { + set_test(THREAD_CREATE_MESSAGE, false); + set_test(THREAD_DELETE, false); + set_test(THREAD_DELETE_EVENT, false); + bot.thread_create_with_message("test", message.channel_id, message.id, 60, 60, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (callback.is_error()) { + set_thread_tested(); + } + else { + auto thread = callback.get(); + thread_id = thread.id; + set_test(THREAD_CREATE_MESSAGE, true); + bot.channel_delete(thread.id, [this](const dpp::confirmation_callback_t &callback) { + set_test(THREAD_DELETE, !callback.is_error()); + set_thread_tested(); + }); + } + }); + } + + void test_files(const dpp::message &message) { + set_test(MESSAGEFILE, false); + if (message.attachments.size() == 3) { + static constexpr auto check_mimetype = [](const auto &headers, std::string mimetype) { + if (auto it = headers.find("content-type"); it != headers.end()) { + // check that the mime type starts with what we gave : for example discord will change "text/plain" to "text/plain; charset=UTF-8" + return it->second.size() >= mimetype.size() && std::equal(it->second.begin(), it->second.begin() + mimetype.size(), mimetype.begin()); + } + else { + return false; + } + }; + message.attachments[0].download([&](const dpp::http_request_completion_t &callback) { + std::lock_guard lock(mutex); + if (callback.status == 200 && callback.body == "test") { + files_success[0] = true; + } + set_file_tested(0); + }); + message.attachments[1].download([&](const dpp::http_request_completion_t &callback) { + std::lock_guard lock(mutex); + if (callback.status == 200 && check_mimetype(callback.headers, "text/plain") && callback.body == "test") { + files_success[1] = true; + } + set_file_tested(1); + }); + message.attachments[2].download([&](const dpp::http_request_completion_t &callback) { + std::lock_guard lock(mutex); + // do not check the contents here because discord can change compression + if (callback.status == 200 && check_mimetype(callback.headers, "image/png")) { + files_success[2] = true; + } + set_file_tested(2); + }); + } + else { + set_file_tested(0); + set_file_tested(1); + set_file_tested(2); + } + } + + void test_pin() { + if (!extended) { + set_pin_tested(); + return; + } + set_test(MESSAGEPIN, false); + set_test(MESSAGEUNPIN, false); + bot.message_pin(channel_id, message_id, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (!callback.is_error()) { + set_test(MESSAGEPIN, true); + bot.message_unpin(TEST_TEXT_CHANNEL_ID, message_id, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (!callback.is_error()) { + set_test(MESSAGEUNPIN, true); + } + set_pin_tested(); + }); + } + else { + set_pin_tested(); + } + }); + } + + public: + dpp::snowflake thread_id; + + explicit message_test_helper(dpp::cluster &_bot) : bot(_bot) {} + + void run(const dpp::message &message) { + pin_tested = false; + thread_tested = false; + files_tested = {false, false, false}; + files_success = {false, false, false}; + channel_id = message.channel_id; + message_id = message.id; + test_pin(); + test_files(message); + test_threads(message); + } + }; + + message_test_helper message_helper(bot); + + class thread_test_helper { + public: + enum event_flag { + MESSAGE_CREATE = 1 << 0, + MESSAGE_EDIT = 1 << 1, + MESSAGE_REACT = 1 << 2, + MESSAGE_REMOVE_REACT = 1 << 3, + MESSAGE_DELETE = 1 << 4, + EVENT_END = 1 << 5 + }; + private: + std::mutex mutex; + dpp::cluster ⊥ + bool edit_tested = false; + bool members_tested = false; + bool messages_tested = false; + bool events_tested = false; + bool get_active_tested = false; + uint32_t events_tested_mask = 0; + uint32_t events_to_test_mask = 0; + + void delete_if_done() { + if (edit_tested && members_tested && messages_tested && events_tested && get_active_tested) { + bot.channel_delete(thread_id); + } + } + + void set_events_tested() { + if (events_tested) { + return; + } + events_tested = true; + delete_if_done(); + } + + void set_edit_tested() { + if (edit_tested) { + return; + } + edit_tested = true; + delete_if_done(); + } + + void set_members_tested() { + if (members_tested) { + return; + } + members_tested = true; + delete_if_done(); + } + + void set_get_active_tested() { + if (get_active_tested) { + return; + } + get_active_tested = true; + delete_if_done(); + } + + void set_messages_tested() { + if (messages_tested) { + return; + } + messages_tested = true; + delete_if_done(); + } + + void set_event_tested(event_flag flag) { + if (events_tested_mask & flag) { + return; + } + events_tested_mask |= flag; + for (uint32_t i = 1; i < EVENT_END; i <<= 1) { + if ((events_to_test_mask & i) && (events_tested_mask & i) != i) { + return; + } + } + set_events_tested(); + } + + void events_abort() { + events_tested_mask |= ~events_to_test_mask; + for (uint32_t i = 1; i < EVENT_END; i <<= 1) { + if ((events_tested_mask & i) != i) { + return; + } + } + set_events_tested(); + } + + public: + /** + * @Brief wrapper for set_event_tested, locking the mutex. Meant to be used from outside the class + */ + void notify_event_tested(event_flag flag) { + std::lock_guard lock{mutex}; + + set_event_tested(flag); + } + + dpp::snowflake thread_id; + + void test_edit(const dpp::thread &thread) { + std::lock_guard lock{mutex}; + + if (!edit_tested) { + dpp::thread edit = thread; + set_test(THREAD_EDIT, false); + set_test(THREAD_UPDATE_EVENT, false); + edit.name = "edited"; + edit.metadata.locked = true; + bot.thread_edit(edit, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (!callback.is_error()) { + set_test(THREAD_EDIT, true); + } + set_edit_tested(); + }); + } + } + + void test_get_active(const dpp::thread &thread) { + std::lock_guard lock{mutex}; + + set_test(THREAD_GET_ACTIVE, false); + bot.threads_get_active(TEST_GUILD_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (!callback.is_error()) { + const auto &threads = callback.get(); + if (auto thread_it = threads.find(thread_id); thread_it != threads.end()) { + const auto &thread = thread_it->second.active_thread; + const auto &member = thread_it->second.bot_member; + if (thread.id == thread_id && member.has_value() && member->user_id == bot.me.id) { + set_test(THREAD_GET_ACTIVE, true); + } + } + } + set_get_active_tested(); + }); + } + + void test_members(const dpp::thread &thread) { + std::lock_guard lock{mutex}; + + if (!members_tested) { + if (!extended) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBER_ADD, false); + set_test(THREAD_MEMBER_GET, false); + set_test(THREAD_MEMBERS_GET, false); + set_test(THREAD_MEMBER_REMOVE, false); + set_test(THREAD_MEMBERS_ADD_EVENT, false); + set_test(THREAD_MEMBERS_REMOVE_EVENT, false); + bot.thread_member_add(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBER_ADD, true); + bot.thread_member_get(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBER_GET, true); + bot.thread_members_get(thread_id, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + set_members_tested(); + return; + } + const auto &members = callback.get(); + if (members.find(TEST_USER_ID) == members.end() || members.find(bot.me.id) == members.end()) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBERS_GET, true); + bot.thread_member_remove(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (!callback.is_error()) { + set_test(THREAD_MEMBER_REMOVE, true); + } + set_members_tested(); + }); + }); + }); + }); + } + } + + void test_messages(const dpp::thread &thread) { + if (!extended) { + set_messages_tested(); + set_events_tested(); + return; + } + + std::lock_guard lock{mutex}; + set_test(THREAD_MESSAGE, false); + set_test(THREAD_MESSAGE_CREATE_EVENT, false); + set_test(THREAD_MESSAGE_EDIT_EVENT, false); + set_test(THREAD_MESSAGE_REACT_ADD_EVENT, false); + set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, false); + set_test(THREAD_MESSAGE_DELETE_EVENT, false); + events_to_test_mask |= MESSAGE_CREATE; + bot.message_create(dpp::message{"hello thread"}.set_channel_id(thread.id), [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + auto m = callback.get(); + m.content = "hello thread?"; + events_to_test_mask |= MESSAGE_EDIT; + bot.message_edit(m, [this, message_id = m.id](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + events_to_test_mask |= MESSAGE_REACT; + bot.message_add_reaction(message_id, thread_id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + events_to_test_mask |= MESSAGE_REMOVE_REACT; + bot.message_delete_reaction(message_id, thread_id, bot.me.id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + events_to_test_mask |= MESSAGE_DELETE; + bot.message_delete(message_id, thread_id, [this] (const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + set_messages_tested(); + if (callback.is_error()) { + events_abort(); + return; + } + set_test(THREAD_MESSAGE, true); + }); + }); + }); + }); + }); + } + + void run(const dpp::thread &thread) { + thread_id = thread.id; + test_get_active(thread); + test_edit(thread); + test_members(thread); + test_messages(thread); + } + + explicit thread_test_helper(dpp::cluster &bot_) : bot{bot_} + { + } + }; + + thread_test_helper thread_helper(bot); + + bot.on_thread_create([&](const dpp::thread_create_t &event) { + if (event.created.name == "thread test") { + set_test(THREAD_CREATE_EVENT, true); + thread_helper.run(event.created); + } + }); + + bool message_tested = false; + bot.on_message_create([&](const dpp::message_create_t & event) { + if (event.msg.author.id == bot.me.id) { + if (event.msg.content == "test message" && !message_tested) { + message_tested = true; + set_test(MESSAGERECEIVE, true); + message_helper.run(event.msg); + set_test(MESSAGESGET, false); + bot.messages_get(event.msg.channel_id, 0, event.msg.id, 0, 5, [](const dpp::confirmation_callback_t &cc){ + if (!cc.is_error()) { + dpp::message_map mm = std::get(cc.value); + if (mm.size()) { + set_test(MESSAGESGET, true); + set_test(TIMESTAMP, false); + dpp::message m = mm.begin()->second; + if (m.sent > 0) { + set_test(TIMESTAMP, true); + } else { + set_test(TIMESTAMP, false); + } + } else { + set_test(MESSAGESGET, false); + } + } else { + set_test(MESSAGESGET, false); + } + }); + set_test(MSGCREATESEND, false); + event.send("MSGCREATESEND", [&bot, ch_id = event.msg.channel_id] (const auto& cc) { + if (!cc.is_error()) { + dpp::message m = std::get(cc.value); + if (m.channel_id == ch_id) { + set_test(MSGCREATESEND, true); + } else { + bot.log(dpp::ll_debug, cc.http_info.body); + set_test(MSGCREATESEND, false); + } + bot.message_delete(m.id, m.channel_id); + } else { + bot.log(dpp::ll_debug, cc.http_info.body); + set_test(MSGCREATESEND, false); + } + }); + } + if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread") { + set_test(THREAD_MESSAGE_CREATE_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_CREATE); + } + } + }); + + bot.on_message_reaction_add([&](const dpp::message_reaction_add_t & event) { + if (event.reacting_user.id == bot.me.id) { + if (event.reacting_emoji.name == "😄") { + set_test(REACTEVENT, true); + } + if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { + set_test(THREAD_MESSAGE_REACT_ADD_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REACT); + } + } + }); + + bot.on_message_reaction_remove([&](const dpp::message_reaction_remove_t & event) { + if (event.reacting_user_id == bot.me.id) { + if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { + set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REMOVE_REACT); + } + } + }); + + bot.on_message_delete([&](const dpp::message_delete_t & event) { + if (event.channel_id == thread_helper.thread_id) { + set_test(THREAD_MESSAGE_DELETE_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_DELETE); + } + }); + + bool message_edit_tested = false; + bot.on_message_update([&](const dpp::message_update_t &event) { + if (event.msg.author == bot.me.id) { + if (event.msg.content == "test edit" && !message_edit_tested) { + message_edit_tested = true; + set_test(EDITEVENT, true); + } + if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread?") { + set_test(THREAD_MESSAGE_EDIT_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_EDIT); + } + } + }); + + bot.on_thread_update([&](const dpp::thread_update_t &event) { + if (event.updating_guild->id == TEST_GUILD_ID && event.updated.id == thread_helper.thread_id && event.updated.name == "edited") { + set_test(THREAD_UPDATE_EVENT, true); + } + }); + + bot.on_thread_members_update([&](const dpp::thread_members_update_t &event) { + if (event.updating_guild->id == TEST_GUILD_ID && event.thread_id == thread_helper.thread_id) { + if (std::find_if(std::begin(event.added), std::end(event.added), is_owner) != std::end(event.added)) { + set_test(THREAD_MEMBERS_ADD_EVENT, true); + } + if (std::find_if(std::begin(event.removed_ids), std::end(event.removed_ids), is_owner) != std::end(event.removed_ids)) { + set_test(THREAD_MEMBERS_REMOVE_EVENT, true); + } + } + }); + + bot.on_thread_delete([&](const dpp::thread_delete_t &event) { + if (event.deleting_guild->id == TEST_GUILD_ID && event.deleted.id == message_helper.thread_id) { + set_test(THREAD_DELETE_EVENT, true); + } + }); + + // set to execute from this thread (main thread) after on_ready is fired + auto do_online_tests = [&] { + coro_online_tests(&bot); + set_test(GUILD_BAN_CREATE, false); + set_test(GUILD_BAN_GET, false); + set_test(GUILD_BANS_GET, false); + set_test(GUILD_BAN_DELETE, false); + if (!offline) { + // some deleted discord accounts to test the ban stuff with... + dpp::snowflake deadUser1(802670069523415057); + dpp::snowflake deadUser2(875302419335094292); + dpp::snowflake deadUser3(1048247361903792198); + + bot.set_audit_reason("ban reason one").guild_ban_add(TEST_GUILD_ID, deadUser1, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) bot.guild_ban_add(TEST_GUILD_ID, deadUser2, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) bot.set_audit_reason("ban reason three").guild_ban_add(TEST_GUILD_ID, deadUser3, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + set_test(GUILD_BAN_CREATE, true); + // when created, continue with getting and deleting + + // get ban + bot.guild_get_ban(TEST_GUILD_ID, deadUser1, [deadUser1](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + dpp::ban ban = event.get(); + if (ban.user_id == deadUser1 && ban.reason == "ban reason one") { + set_test(GUILD_BAN_GET, true); + } + } + }); + + // get multiple bans + bot.guild_get_bans(TEST_GUILD_ID, 0, deadUser1, 3, [deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + dpp::ban_map bans = event.get(); + int successCount = 0; + for (auto &ban: bans) { + if (ban.first == ban.second.user_id) { // the key should match the ban's user_id + if (ban.first == deadUser2 && ban.second.reason.empty()) { + successCount++; + } else if (ban.first == deadUser3 && ban.second.reason == "ban reason three") { + successCount++; + } + } + } + if (successCount == 2) { + set_test(GUILD_BANS_GET, true); + } + } + }); + + // unban them + bot.guild_ban_delete(TEST_GUILD_ID, deadUser1, [&bot, deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + bot.guild_ban_delete(TEST_GUILD_ID, deadUser2, [&bot, deadUser3](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + bot.guild_ban_delete(TEST_GUILD_ID, deadUser3, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(GUILD_BAN_DELETE, true); + } + }); + } + }); + } + }); + }); + }); + }); + } + + set_test(REQUEST_GET_IMAGE, false); + if (!offline) { + bot.request("https://dpp.dev/DPP-Logo.png", dpp::m_get, [&bot](const dpp::http_request_completion_t &callback) { + if (callback.status != 200) { + return; + } + set_test(REQUEST_GET_IMAGE, true); + + dpp::emoji emoji; + emoji.load_image(callback.body, dpp::i_png); + emoji.name = "dpp"; + + // emoji unit test with the requested image + set_test(EMOJI_CREATE, false); + set_test(EMOJI_GET, false); + set_test(EMOJI_DELETE, false); + bot.guild_emoji_create(TEST_GUILD_ID, emoji, [&bot](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + set_test(EMOJI_CREATE, true); + + auto created = event.get(); + bot.guild_emoji_get(TEST_GUILD_ID, created.id, [&bot, created](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto fetched = event.get(); + if (created.id == fetched.id && created.name == fetched.name && created.flags == fetched.flags) { + set_test(EMOJI_GET, true); + } + + bot.guild_emoji_delete(TEST_GUILD_ID, fetched.id, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(EMOJI_DELETE, true); + } + }); + }); + }); + }); + } + + set_test(INVITE_CREATE, false); + set_test(INVITE_GET, false); + set_test(INVITE_DELETE, false); + if (!offline) { + dpp::channel channel; + channel.id = TEST_TEXT_CHANNEL_ID; + dpp::invite invite; + invite.max_age = 0; + invite.max_uses = 100; + set_test(INVITE_CREATE_EVENT, false); + bot.channel_invite_create(channel, invite, [&bot, invite](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + + auto created = event.get(); + if (!created.code.empty() && created.channel_id == TEST_TEXT_CHANNEL_ID && created.guild_id == TEST_GUILD_ID && created.inviter.id == bot.me.id) { + set_test(INVITE_CREATE, true); + } + + bot.invite_get(created.code, [&bot, created](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + auto retrieved = event.get(); + if (retrieved.code == created.code && retrieved.guild_id == created.guild_id && retrieved.channel_id == created.channel_id && retrieved.inviter.id == created.inviter.id) { + if (retrieved.destination_guild.flags & dpp::g_community) { + set_test(INVITE_GET, retrieved.expires_at == 0); + } else { + set_test(INVITE_GET, true); + } + + } else { + set_test(INVITE_GET, false); + } + } else { + set_test(INVITE_GET, false); + } + + set_test(INVITE_DELETE_EVENT, false); + bot.invite_delete(created.code, [](const dpp::confirmation_callback_t &event) { + set_test(INVITE_DELETE, !event.is_error()); + }); + }); + }); + } + + set_test(AUTOMOD_RULE_CREATE, false); + set_test(AUTOMOD_RULE_GET, false); + set_test(AUTOMOD_RULE_GET_ALL, false); + set_test(AUTOMOD_RULE_DELETE, false); + if (!offline) { + dpp::automod_rule automodRule; + automodRule.name = "automod rule (keyword type)"; + automodRule.trigger_type = dpp::amod_type_keyword; + dpp::automod_metadata metadata1; + metadata1.keywords.emplace_back("*cat*"); + metadata1.keywords.emplace_back("train"); + metadata1.keywords.emplace_back("*.exe"); + metadata1.regex_patterns.emplace_back("^[^a-z]$"); + metadata1.allow_list.emplace_back("@silent*"); + automodRule.trigger_metadata = metadata1; + dpp::automod_action automodAction; + automodAction.type = dpp::amod_action_timeout; + automodAction.duration_seconds = 6000; + automodRule.actions.emplace_back(automodAction); + + bot.automod_rules_get(TEST_GUILD_ID, [&bot, automodRule](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto rules = event.get(); + set_test(AUTOMOD_RULE_GET_ALL, true); + for (const auto &rule: rules) { + if (rule.second.trigger_type == dpp::amod_type_keyword) { + // delete one automod rule of type KEYWORD before creating one to make space... + bot.automod_rule_delete(TEST_GUILD_ID, rule.first); + } + } + + // start creating the automod rules + bot.automod_rule_create(TEST_GUILD_ID, automodRule, [&bot, automodRule](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto created = event.get(); + if (created.name == automodRule.name) { + set_test(AUTOMOD_RULE_CREATE, true); + } + + // get automod rule + bot.automod_rule_get(TEST_GUILD_ID, created.id, [automodRule, &bot, created](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto retrieved = event.get(); + if (retrieved.name == automodRule.name && + retrieved.trigger_type == automodRule.trigger_type && + retrieved.trigger_metadata.keywords == automodRule.trigger_metadata.keywords && + retrieved.trigger_metadata.regex_patterns == automodRule.trigger_metadata.regex_patterns && + retrieved.trigger_metadata.allow_list == automodRule.trigger_metadata.allow_list && retrieved.actions.size() == automodRule.actions.size()) { + set_test(AUTOMOD_RULE_GET, true); + } + + // delete the automod rule + bot.automod_rule_delete(TEST_GUILD_ID, retrieved.id, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(AUTOMOD_RULE_DELETE, true); + } + }); + }); + }); + }); + } + + set_test(USER_GET, false); + set_test(USER_GET_FLAGS, false); + if (!offline) { + bot.user_get(TEST_USER_ID, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + auto u = std::get(event.value); + if (u.id == TEST_USER_ID) { + set_test(USER_GET, true); + } else { + set_test(USER_GET, false); + } + json j = json::parse(event.http_info.body); + uint64_t raw_flags = j["public_flags"]; + if (j.contains("flags")) { + uint64_t flags = j["flags"]; + raw_flags |= flags; + } + // testing all user flags from https://discord.com/developers/docs/resources/user#user-object-user-flags + // they're manually set here because the dpp::user_flags don't match to the discord API, so we can't use them to compare with the raw flags! + if ( + u.is_discord_employee() == ((raw_flags & (1 << 0)) != 0) && + u.is_partnered_owner() == ((raw_flags & (1 << 1)) != 0) && + u.has_hypesquad_events() == ((raw_flags & (1 << 2)) != 0) && + u.is_bughunter_1() == ((raw_flags & (1 << 3)) != 0) && + u.is_house_bravery() == ((raw_flags & (1 << 6)) != 0) && + u.is_house_brilliance() == ((raw_flags & (1 << 7)) != 0) && + u.is_house_balance() == ((raw_flags & (1 << 8)) != 0) && + u.is_early_supporter() == ((raw_flags & (1 << 9)) != 0) && + u.is_team_user() == ((raw_flags & (1 << 10)) != 0) && + u.is_bughunter_2() == ((raw_flags & (1 << 14)) != 0) && + u.is_verified_bot() == ((raw_flags & (1 << 16)) != 0) && + u.is_verified_bot_dev() == ((raw_flags & (1 << 17)) != 0) && + u.is_certified_moderator() == ((raw_flags & (1 << 18)) != 0) && + u.is_bot_http_interactions() == ((raw_flags & (1 << 19)) != 0) && + u.is_active_developer() == ((raw_flags & (1 << 22)) != 0) + ) { + set_test(USER_GET_FLAGS, true); + } else { + set_test(USER_GET_FLAGS, false); + } + } else { + set_test(USER_GET, false); + set_test(USER_GET_FLAGS, false); + } + }); + } + + set_test(VOICE_CHANNEL_CREATE, false); + set_test(VOICE_CHANNEL_EDIT, false); + set_test(VOICE_CHANNEL_DELETE, false); + if (!offline) { + dpp::channel channel1; + channel1.set_type(dpp::CHANNEL_VOICE) + .set_guild_id(TEST_GUILD_ID) + .set_name("voice1") + .add_permission_overwrite(TEST_GUILD_ID, dpp::ot_role, 0, dpp::p_view_channel) + .set_user_limit(99); + dpp::channel createdChannel; + try { + createdChannel = bot.channel_create_sync(channel1); + } catch (dpp::rest_exception &exception) { + set_test(VOICE_CHANNEL_CREATE, false); + } + if (createdChannel.name == channel1.name && + createdChannel.user_limit == 99 && + createdChannel.name == "voice1") { + for (auto overwrite: createdChannel.permission_overwrites) { + if (overwrite.id == TEST_GUILD_ID && overwrite.type == dpp::ot_role && overwrite.deny == dpp::p_view_channel) { + set_test(VOICE_CHANNEL_CREATE, true); + } + } + + // edit the voice channel + createdChannel.set_name("foobar2"); + createdChannel.set_user_limit(2); + for (auto overwrite: createdChannel.permission_overwrites) { + if (overwrite.id == TEST_GUILD_ID) { + overwrite.deny.set(0); + overwrite.allow.set(dpp::p_view_channel); + } + } + try { + dpp::channel edited = bot.channel_edit_sync(createdChannel); + if (edited.name == "foobar2" && edited.user_limit == 2) { + set_test(VOICE_CHANNEL_EDIT, true); + } + } catch (dpp::rest_exception &exception) { + set_test(VOICE_CHANNEL_EDIT, false); + } + + // delete the voice channel + try { + bot.channel_delete_sync(createdChannel.id); + set_test(VOICE_CHANNEL_DELETE, true); + } catch (dpp::rest_exception &exception) { + set_test(VOICE_CHANNEL_DELETE, false); + } + } + } + + set_test(FORUM_CREATION, false); + set_test(FORUM_CHANNEL_GET, false); + set_test(FORUM_CHANNEL_DELETE, false); + if (!offline) { + dpp::channel c; + c.name = "test-forum-channel"; + c.guild_id = TEST_GUILD_ID; + c.set_topic("This is a forum channel"); + c.set_flags(dpp::CHANNEL_FORUM); + c.default_sort_order = dpp::so_creation_date; + dpp::forum_tag t; + t.name = "Alpha"; + t.emoji = "❌"; + c.available_tags = {t}; + c.default_auto_archive_duration = dpp::arc_1_day; + c.default_reaction = "✅"; + c.default_thread_rate_limit_per_user = 10; + bot.channel_create(c, [&bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(FORUM_CREATION, true); + auto channel = std::get(event.value); + // retrieve the forum channel and check the values + bot.channel_get(channel.id, [forum_id = channel.id, &bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + auto channel = std::get(event.value); + bot.log(dpp::ll_debug, event.http_info.body); + bool tag = false; + for (auto &t : channel.available_tags) { + if (t.name == "Alpha" && std::holds_alternative(t.emoji) && std::get(t.emoji) == "❌") { + tag = true; + } + } + bool name = channel.name == "test-forum-channel"; + bool sort = channel.default_sort_order == dpp::so_creation_date; + bool rateLimit = channel.default_thread_rate_limit_per_user == 10; + set_test(FORUM_CHANNEL_GET, tag && name && sort && rateLimit); + } else { + set_test(FORUM_CHANNEL_GET, false); + } + // delete the forum channel + bot.channel_delete(forum_id, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(FORUM_CHANNEL_DELETE, true); + } else { + set_test(FORUM_CHANNEL_DELETE, false); + } + }); + }); + } else { + set_test(FORUM_CREATION, false); + set_test(FORUM_CHANNEL_GET, false); + } + }); + } + + set_test(THREAD_CREATE, false); + if (!offline) { + bot.thread_create("thread test", TEST_TEXT_CHANNEL_ID, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 60, [&](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + [[maybe_unused]] const auto &thread = event.get(); + set_test(THREAD_CREATE, true); + } + // the thread tests are in the on_thread_create event handler + }); + } + + set_test(MEMBER_GET, false); + if (!offline) { + bot.guild_get_member(TEST_GUILD_ID, TEST_USER_ID, [](const dpp::confirmation_callback_t &event){ + if (!event.is_error()) { + dpp::guild_member m = std::get(event.value); + if (m.guild_id == TEST_GUILD_ID && m.user_id == TEST_USER_ID) { + set_test(MEMBER_GET, true); + } else { + set_test(MEMBER_GET, false); + } + } else { + set_test(MEMBER_GET, false); + } + }); + } + + set_test(ROLE_CREATE, false); + set_test(ROLE_EDIT, false); + set_test(ROLE_DELETE, false); + if (!offline) { + dpp::role r; + r.guild_id = TEST_GUILD_ID; + r.name = "Test-Role"; + r.permissions.add(dpp::p_move_members); + r.set_flags(dpp::r_mentionable); + r.colour = dpp::colors::moon_yellow; + dpp::role createdRole; + try { + createdRole = bot.role_create_sync(r); + if (createdRole.name == r.name && + createdRole.has_move_members() && + createdRole.flags & dpp::r_mentionable && + createdRole.colour == r.colour) { + set_test(ROLE_CREATE, true); + } + } catch (dpp::rest_exception &exception) { + set_test(ROLE_CREATE, false); + } + createdRole.guild_id = TEST_GUILD_ID; + createdRole.name = "Test-Role-Edited"; + createdRole.colour = dpp::colors::light_sea_green; + try { + dpp::role edited = bot.role_edit_sync(createdRole); + if (createdRole.id == edited.id && edited.name == "Test-Role-Edited") { + set_test(ROLE_EDIT, true); + } + } catch (dpp::rest_exception &exception) { + set_test(ROLE_EDIT, false); + } + try { + bot.role_delete_sync(TEST_GUILD_ID, createdRole.id); + set_test(ROLE_DELETE, true); + } catch (dpp::rest_exception &exception) { + set_test(ROLE_DELETE, false); + } + } + }; + + set_test(BOTSTART, false); + try { + if (!offline) { + bot.start(true); + set_test(BOTSTART, true); + } + } + catch (const std::exception &) { + set_test(BOTSTART, false); + } + + set_test(TIMERSTART, false); + uint32_t ticks = 0; + dpp::timer th = bot.start_timer([&](dpp::timer timer_handle) { + if (ticks == 5) { + /* The simple test timer ticks every second. + * If we get to 5 seconds, we know the timer is working. + */ + set_test(TIMERSTART, true); + } + ticks++; + }, 1); + + set_test(TIMEDLISTENER, false); + dpp::timed_listener tl(&bot, 10, bot.on_log, [&](const dpp::log_t & event) { + set_test(TIMEDLISTENER, true); + }); + + set_test(ONESHOT, false); + bool once = false; + dpp::oneshot_timer ost(&bot, 5, [&](dpp::timer timer_handle) { + if (!once) { + set_test(ONESHOT, true); + } else { + set_test(ONESHOT, false); + } + once = true; + }); + + // online tests + if (!offline) { + if (std::future_status status = ready_future.wait_for(std::chrono::seconds(20)); status != std::future_status::timeout) { + do_online_tests(); + } + } + + noparam_api_test(current_user_get, dpp::user_identified, CURRENTUSER); + singleparam_api_test(channel_get, TEST_TEXT_CHANNEL_ID, dpp::channel, GETCHAN); + singleparam_api_test(guild_get, TEST_GUILD_ID, dpp::guild, GETGUILD); + singleparam_api_test_list(roles_get, TEST_GUILD_ID, dpp::role_map, GETROLES); + singleparam_api_test_list(channels_get, TEST_GUILD_ID, dpp::channel_map, GETCHANS); + singleparam_api_test_list(guild_get_invites, TEST_GUILD_ID, dpp::invite_map, GETINVS); + multiparam_api_test_list(guild_get_bans, TEST_GUILD_ID, dpp::ban_map, GETBANS); + singleparam_api_test_list(channel_pins_get, TEST_TEXT_CHANNEL_ID, dpp::message_map, GETPINS); + singleparam_api_test_list(guild_events_get, TEST_GUILD_ID, dpp::scheduled_event_map, GETEVENTS); + twoparam_api_test(guild_event_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::scheduled_event, GETEVENT); + twoparam_api_test_list(guild_event_users_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::event_member_map, GETEVENTUSERS); + + std::this_thread::sleep_for(std::chrono::seconds(20)); + + /* Test stopping timer */ + set_test(TIMERSTOP, false); + set_test(TIMERSTOP, bot.stop_timer(th)); + + set_test(USERCACHE, false); + if (!offline) { + dpp::user* u = dpp::find_user(TEST_USER_ID); + set_test(USERCACHE, u); + } + set_test(CHANNELCACHE, false); + set_test(CHANNELTYPES, false); + if (!offline) { + dpp::channel* c = dpp::find_channel(TEST_TEXT_CHANNEL_ID); + dpp::channel* c2 = dpp::find_channel(TEST_VC_ID); + set_test(CHANNELCACHE, c && c2); + set_test(CHANNELTYPES, c && c->is_text_channel() && !c->is_voice_channel() && c2 && c2->is_voice_channel() && !c2->is_text_channel()); + } + + wait_for_tests(); + + } + catch (const std::exception &e) { + std::cout << e.what() << "\n"; + set_test(CLUSTER, false); + } +} diff --git a/src/unittest/http.cpp b/src/unittest/http.cpp new file mode 100644 index 0000000000..6f8639b86a --- /dev/null +++ b/src/unittest/http.cpp @@ -0,0 +1,93 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include "test.h" + +/* Unit tests for HTTPS client */ +void http_client_tests(const std::string& token) { + dpp::http_connect_info hci; + set_test(HOSTINFO, false); + + hci = dpp::https_client::get_host_info("https://test.com:444"); + bool hci_test = (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 444 && hci.is_ssl == true); + + hci = dpp::https_client::get_host_info("https://test.com"); + hci_test = hci_test && (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 443 && hci.is_ssl == true); + + hci = dpp::https_client::get_host_info("http://test.com"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); + + hci = dpp::https_client::get_host_info("http://test.com:90"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 90 && hci.is_ssl == false); + + hci = dpp::https_client::get_host_info("test.com:97"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 97 && hci.is_ssl == false); + + hci = dpp::https_client::get_host_info("test.com"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); + + set_test(HOSTINFO, hci_test); + + set_test(HTTPS, false); + if (!offline) { + dpp::multipart_content multipart = dpp::https_client::build_multipart( + "{\"content\":\"test\"}", {"test.txt", "blob.blob"}, {"ABCDEFGHI", "BLOB!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"}, {"text/plain", "application/octet-stream"} + ); + try { + dpp::https_client c("discord.com", 443, "/api/channels/" + std::to_string(TEST_TEXT_CHANNEL_ID) + "/messages", "POST", multipart.body, + { + {"Content-Type", multipart.mimetype}, + {"Authorization", "Bot " + token} + } + ); + std::string hdr1 = c.get_header("server"); + set_test(HTTPS, hdr1 == "cloudflare" && c.get_status() == 200); + } + catch (const dpp::exception& e) { + std::cout << e.what() << "\n"; + set_test(HTTPS, false); + } + } + + set_test(HTTP, false); + try { + dpp::https_client c2("github.com", 80, "/", "GET", "", {}, true); + std::string hdr2 = c2.get_header("location"); + set_test(HTTP, hdr2 == "https://github.com/" && c2.get_status() == 301); + } + catch (const dpp::exception& e) { + std::cout << e.what() << "\n"; + set_test(HTTP, false); + } + + set_test(MULTIHEADER, false); + try { + dpp::https_client c2("www.google.com", 80, "/", "GET", "", {}, true); + size_t count = c2.get_header_count("set-cookie"); + size_t count_list = c2.get_header_list("set-cookie").size(); + // Google sets a bunch of cookies when we start accessing it. + set_test(MULTIHEADER, c2.get_status() == 200 && count > 1 && count == count_list); + } + catch (const dpp::exception& e) { + std::cout << e.what() << "\n"; + set_test(MULTIHEADER, false); + } +} diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 086e0336f9..8270d0687b 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -21,49 +21,6 @@ ************************************************************************************/ #include "test.h" -#include -#include -#include -#include - -/** - * @brief Type trait to check if a certain type has a build_json method - * - * @tparam T type to check for - */ -template > -struct has_build_json : std::false_type {}; - -template -struct has_build_json().build_json())>> : std::true_type {}; - -/** - * @brief Type trait to check if a certain type has a build_json method - * - * @tparam T type to check for - */ -template -constexpr bool has_build_json_v = has_build_json::value; - -/** - * @brief Type trait to check if a certain type has a fill_from_json method - * - * @tparam T type to check for - */ -template -struct has_fill_from_json : std::false_type {}; - -template -struct has_fill_from_json().fill_from_json(std::declval()))>> : std::true_type {}; - -/** - * @brief Type trait to check if a certain type has a fill_from_json method - * - * @tparam T type to check for - */ -template -constexpr bool has_fill_from_json_v = has_fill_from_json::value; - /* Unit tests go here */ int main(int argc, char *argv[]) { @@ -79,2161 +36,19 @@ int main(int argc, char *argv[]) std::cout << "Running offline and " << (extended ? "extended" : "limited") << " online unit tests. Guild ID: " << TEST_GUILD_ID << " Text Channel ID: " << TEST_TEXT_CHANNEL_ID << " VC ID: " << TEST_VC_ID << " User ID: " << TEST_USER_ID << " Event ID: " << TEST_EVENT_ID << "\n"; } - std::string test_to_escape = "*** _This is a test_ ***\n```cpp\n\ -int main() {\n\ - /* Comment */\n\ - int answer = 42;\n\ - return answer; // ___\n\ -};\n\ -```\n\ -Markdown lol ||spoiler|| ~~strikethrough~~ `small *code* block`\n"; - - set_test(COMPARISON, false); - dpp::user u1; - dpp::user u2; - dpp::user u3; - u1.id = u2.id = 666; - u3.id = 777; - set_test(COMPARISON, u1 == u2 && u1 != u3); - - - set_test(ERRORS, false); - - /* Prepare a confirmation_callback_t in error state (400) */ - dpp::confirmation_callback_t error_test; - bool error_message_success = false; - error_test.http_info.status = 400; - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"options\": {\ - \"0\": {\ - \"name\": {\ - \"_errors\": [\ - {\ - \"code\": \"STRING_TYPE_REGEX\",\ - \"message\": \"String value did not match validation regex.\"\ - },\ - {\ - \"code\": \"APPLICATION_COMMAND_INVALID_NAME\",\ - \"message\": \"Command name is invalid\"\ - }\ - ]\ - }\ - }\ - }\ - }\ - }"; - error_message_success = (error_test.get_error().human_readable == "50035: Invalid Form Body\n\t- options[0].name: String value did not match validation regex. (STRING_TYPE_REGEX)\n\t- options[0].name: Command name is invalid (APPLICATION_COMMAND_INVALID_NAME)"); - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"type\": {\ - \"_errors\": [\ - {\ - \"code\": \"BASE_TYPE_CHOICES\",\ - \"message\": \"Value must be one of {4, 5, 9, 10, 11}.\"\ - }\ - ]\ - }\ - }\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - type: Value must be one of {4, 5, 9, 10, 11}. (BASE_TYPE_CHOICES)"); - - error_test.http_info.body = "{\ - \"message\": \"Unknown Guild\",\ - \"code\": 10004\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "10004: Unknown Guild"); - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"allowed_mentions\": {\ - \"_errors\": [\ - {\ - \"code\": \"MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE\",\ - \"message\": \"parse:[\\\"users\\\"] and users: [ids...] are mutually exclusive.\"\ - }\ - ]\ - }\ - }\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - allowed_mentions: parse:[\"users\"] and users: [ids...] are mutually exclusive. (MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE)"); - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"1\": {\ - \"options\": {\ - \"1\": {\ - \"description\": {\ - \"_errors\": [\ - {\ - \"code\": \"BASE_TYPE_BAD_LENGTH\",\ - \"message\": \"Must be between 1 and 100 in length.\"\ - }\ - ]\ - }\ - }\ - }\ - }\ - }\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - [1].options[1].description: Must be between 1 and 100 in length. (BASE_TYPE_BAD_LENGTH)"); - - set_test(ERRORS, error_message_success); - - set_test(MD_ESC_1, false); - set_test(MD_ESC_2, false); - std::string escaped1 = dpp::utility::markdown_escape(test_to_escape); - std::string escaped2 = dpp::utility::markdown_escape(test_to_escape, true); - set_test(MD_ESC_1, escaped1 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ -```cpp\n\ -int main() {\n\ - /* Comment */\n\ - int answer = 42;\n\ - return answer; // ___\n\ -};\n\ -```\n\ -Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ `small *code* block`\n"); - set_test(MD_ESC_2, escaped2 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ -\\`\\`\\`cpp\n\ -int main\\(\\) {\n\ - /\\* Comment \\*/\n\ - int answer = 42;\n\ - return answer; // \\_\\_\\_\n\ -};\n\ -\\`\\`\\`\n\ -Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* block\\`\n"); - - set_test(URLENC, false); - set_test(URLENC, dpp::utility::url_encode("ABC123_+\\|$*/AAA[]😄") == "ABC123_%2B%5C%7C%24%2A%2FAAA%5B%5D%F0%9F%98%84"); - - set_test(BASE64ENC, false); - set_test(BASE64ENC, - dpp::base64_encode(reinterpret_cast("a"), 1) == "YQ==" && - dpp::base64_encode(reinterpret_cast("bc"), 2) == "YmM=" && - dpp::base64_encode(reinterpret_cast("def"), 3) == "ZGVm" && - dpp::base64_encode(reinterpret_cast("ghij"), 4) == "Z2hpag==" && - dpp::base64_encode(reinterpret_cast("klmno"), 5) == "a2xtbm8=" && - dpp::base64_encode(reinterpret_cast("pqrstu"), 6) == "cHFyc3R1" && - dpp::base64_encode(reinterpret_cast("vwxyz12"), 7) == "dnd4eXoxMg==" - ); - - dpp::http_connect_info hci; - set_test(HOSTINFO, false); - - hci = dpp::https_client::get_host_info("https://test.com:444"); - bool hci_test = (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 444 && hci.is_ssl == true); - - hci = dpp::https_client::get_host_info("https://test.com"); - hci_test = hci_test && (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 443 && hci.is_ssl == true); - - hci = dpp::https_client::get_host_info("http://test.com"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); - - hci = dpp::https_client::get_host_info("http://test.com:90"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 90 && hci.is_ssl == false); - - hci = dpp::https_client::get_host_info("test.com:97"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 97 && hci.is_ssl == false); - - hci = dpp::https_client::get_host_info("test.com"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); - - set_test(HOSTINFO, hci_test); - - set_test(HTTPS, false); - if (!offline) { - dpp::multipart_content multipart = dpp::https_client::build_multipart( - "{\"content\":\"test\"}", {"test.txt", "blob.blob"}, {"ABCDEFGHI", "BLOB!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"}, {"text/plain", "application/octet-stream"} - ); - try { - dpp::https_client c("discord.com", 443, "/api/channels/" + std::to_string(TEST_TEXT_CHANNEL_ID) + "/messages", "POST", multipart.body, - { - {"Content-Type", multipart.mimetype}, - {"Authorization", "Bot " + token} - } - ); - std::string hdr1 = c.get_header("server"); - std::string content1 = c.get_content(); - set_test(HTTPS, hdr1 == "cloudflare" && c.get_status() == 200); - } - catch (const dpp::exception& e) { - std::cout << e.what() << "\n"; - set_test(HTTPS, false); - } - } - - set_test(HTTP, false); - try { - dpp::https_client c2("github.com", 80, "/", "GET", "", {}, true); - std::string hdr2 = c2.get_header("location"); - std::string content2 = c2.get_content(); - set_test(HTTP, hdr2 == "https://github.com/" && c2.get_status() == 301); - } - catch (const dpp::exception& e) { - std::cout << e.what() << "\n"; - set_test(HTTP, false); - } - - set_test(MULTIHEADER, false); - try { - dpp::https_client c2("www.google.com", 80, "/", "GET", "", {}, true); - size_t count = c2.get_header_count("set-cookie"); - size_t count_list = c2.get_header_list("set-cookie").size(); - // Google sets a bunch of cookies when we start accessing it. - set_test(MULTIHEADER, c2.get_status() == 200 && count > 1 && count == count_list); - } - catch (const dpp::exception& e) { - std::cout << e.what() << "\n"; - set_test(MULTIHEADER, false); - } - - std::vector testaudio = load_test_audio(); - - set_test(READFILE, false); - std::string rf_test = dpp::utility::read_file(SHARED_OBJECT); - FILE* fp = fopen(SHARED_OBJECT, "rb"); - fseek(fp, 0, SEEK_END); - size_t off = (size_t)ftell(fp); - fclose(fp); - set_test(READFILE, off == rf_test.length()); - - set_test(TIMESTAMPTOSTRING, false); - set_test(TIMESTAMPTOSTRING, dpp::ts_to_string(1642611864) == "2022-01-19T17:04:24Z"); - - set_test(ROLE_COMPARE, false); - dpp::role role_1, role_2; - role_1.position = 1; - role_2.position = 2; - set_test(ROLE_COMPARE, role_1 < role_2 && role_1 != role_2); - - set_test(WEBHOOK, false); - try { - dpp::webhook test_wh("https://discord.com/api/webhooks/833047646548133537/ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE"); - set_test(WEBHOOK, (test_wh.token == "ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE") && (test_wh.id == dpp::snowflake(833047646548133537))); - } - catch (const dpp::exception&) { - set_test(WEBHOOK, false); - } - - { // test dpp::snowflake - start_test(SNOWFLAKE); - bool success = true; - dpp::snowflake s = 69420; - json j; - j["value"] = s; - success = dpp::snowflake_not_null(&j, "value") == 69420 && success; - DPP_CHECK_CONSTRUCT_ASSIGN(SNOWFLAKE, dpp::snowflake, success); - s = 42069; - success = success && (s == 42069 && s == dpp::snowflake{42069} && s == "42069"); - success = success && (dpp::snowflake{69} < dpp::snowflake{420} && (dpp::snowflake{69} < 420)); - s = "69420"; - success = success && s == 69420; - auto conversion_test = [](dpp::snowflake sl) { - return sl.str(); - }; - s = conversion_test(std::string{"1337"}); - success = success && s == 1337; /* THIS BREAKS (and i do not care very much): && s == conversion_test(dpp::snowflake{"1337"}); */ - success = success && dpp::snowflake{0} == 0; - set_test(SNOWFLAKE, success); - } - - { // test dpp::json_interface - start_test(JSON_INTERFACE); - struct fillable : dpp::json_interface { - fillable &fill_from_json_impl(dpp::json *) { - return *this; - } - }; - struct buildable : dpp::json_interface { - json to_json_impl(bool = false) const { - return {}; - } - }; - struct fillable_and_buildable : dpp::json_interface { - fillable_and_buildable &fill_from_json_impl(dpp::json *) { - return *this; - } - - json to_json_impl(bool = false) const { - return {}; - } - }; - bool success = true; - - DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); - DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); - DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v, success); - - DPP_CHECK(JSON_INTERFACE, !has_build_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); - DPP_CHECK(JSON_INTERFACE, !has_build_json_v, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); - - DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); - set_test(JSON_INTERFACE, success); - } - - { // test interaction_create_t::get_parameter - // create a fake interaction - dpp::cluster cluster(""); - dpp::discord_client client(&cluster, 1, 1, ""); - dpp::interaction_create_t interaction(&client, ""); - - /* Check the method with subcommands */ - set_test(GET_PARAMETER_WITH_SUBCOMMANDS, false); - - dpp::command_interaction cmd_data; // command - cmd_data.type = dpp::ctxm_chat_input; - cmd_data.name = "command"; - - dpp::command_data_option subcommandgroup; // subcommand group - subcommandgroup.name = "group"; - subcommandgroup.type = dpp::co_sub_command_group; - - dpp::command_data_option subcommand; // subcommand - subcommand.name = "add"; - subcommand.type = dpp::co_sub_command; - - dpp::command_data_option option1; // slashcommand option - option1.name = "user"; - option1.type = dpp::co_user; - option1.value = dpp::snowflake(189759562910400512); - - dpp::command_data_option option2; // slashcommand option - option2.name = "checked"; - option2.type = dpp::co_boolean; - option2.value = true; - - // add them - subcommand.options.push_back(option1); - subcommand.options.push_back(option2); - subcommandgroup.options.push_back(subcommand); - cmd_data.options.push_back(subcommandgroup); - interaction.command.data = cmd_data; - - dpp::snowflake value1 = std::get(interaction.get_parameter("user")); - set_test(GET_PARAMETER_WITH_SUBCOMMANDS, value1 == dpp::snowflake(189759562910400512)); - - /* Check the method without subcommands */ - set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, false); - - dpp::command_interaction cmd_data2; // command - cmd_data2.type = dpp::ctxm_chat_input; - cmd_data2.name = "command"; - - dpp::command_data_option option3; // slashcommand option - option3.name = "number"; - option3.type = dpp::co_integer; - option3.value = int64_t(123456); - - cmd_data2.options.push_back(option3); - interaction.command.data = cmd_data2; - - int64_t value2 = std::get(interaction.get_parameter("number")); - set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, value2 == 123456); - } - - { // test dpp::command_option_choice::fill_from_json - set_test(OPTCHOICE_DOUBLE, false); - set_test(OPTCHOICE_INT, false); - set_test(OPTCHOICE_BOOL, false); - set_test(OPTCHOICE_SNOWFLAKE, false); - set_test(OPTCHOICE_STRING, false); - json j; - dpp::command_option_choice choice; - j["value"] = 54.321; - choice.fill_from_json(&j); - bool success_double = std::holds_alternative(choice.value); - j["value"] = 8223372036854775807; - choice.fill_from_json(&j); - bool success_int = std::holds_alternative(choice.value); - j["value"] = -8223372036854775807; - choice.fill_from_json(&j); - bool success_int2 = std::holds_alternative(choice.value); - j["value"] = true; - choice.fill_from_json(&j); - bool success_bool = std::holds_alternative(choice.value); - dpp::snowflake s(845266178036516757); // example snowflake - j["value"] = s; - choice.fill_from_json(&j); - bool success_snowflake = std::holds_alternative(choice.value) && std::get(choice.value) == s; - j["value"] = "foobar"; - choice.fill_from_json(&j); - bool success_string = std::holds_alternative(choice.value); - set_test(OPTCHOICE_DOUBLE, success_double); - set_test(OPTCHOICE_INT, success_int && success_int2); - set_test(OPTCHOICE_BOOL, success_bool); - set_test(OPTCHOICE_SNOWFLAKE, success_snowflake); - set_test(OPTCHOICE_STRING, success_string); - } - - { - set_test(PERMISSION_CLASS, false); - bool success = false; - auto p = dpp::permission(); - p = 16; - success = p == 16; - p |= 4; - success = p == 20 && success; - p <<= 8; // left shift - success = p == 5120 && success; - auto s = std::to_string(p); - success = s == "5120" && success; - p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_administrator); - success = !p.has(dpp::p_administrator, dpp::p_ban_members) && success; // must return false because they're not both set - success = !p.has(dpp::p_administrator | dpp::p_ban_members) && success; - success = p.can(dpp::p_ban_members) && success; - success = p.can(dpp::p_speak) && success; - - constexpr auto permission_test = [](dpp::permission p) constexpr noexcept { - bool success{true}; - - p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_connect); - p.set(dpp::p_administrator, dpp::p_ban_members); - success = p.has(dpp::p_administrator) && success; - success = p.has(dpp::p_administrator) && p.has(dpp::p_ban_members) && success; - success = p.has(dpp::p_administrator, dpp::p_ban_members) && success; - success = p.has(dpp::p_administrator | dpp::p_ban_members) && success; - success = p.add(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; - success = !p.remove(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; - p.remove(dpp::p_administrator); - success = p.can(dpp::p_ban_members) && success; - success = !p.can(dpp::p_speak, dpp::p_ban_members) && success; - success = p.can_any(dpp::p_speak, dpp::p_ban_members) && success; - return success; - }; - constexpr auto constexpr_success = permission_test({~uint64_t{0}}); // test in constant evaluated - success = permission_test({~uint64_t{0}}) && constexpr_success && success; // test at runtime - set_test(PERMISSION_CLASS, success); - } - - { // dpp event classes - start_test(EVENT_CLASS); - bool success = true; - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::log_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_execute_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_state_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::interaction_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::slashcommand_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::button_click_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::form_submit_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::autocomplete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::context_menu_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_context_menu_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_context_menu_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::select_click_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_stickers_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_join_request_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::ready_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::resumed_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::typing_start_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_track_marker_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_members_chunk_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_emoji_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_bulk_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_pins_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_all_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_server_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_emojis_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::presence_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::webhooks_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_integrations_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_audit_log_entry_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_list_sync_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_member_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_members_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_buffer_send_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_user_talking_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_ready_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_receive_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_speaking_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_disconnect_t, success); - set_test(EVENT_CLASS, success); - } - - - { // some dpp::user methods - dpp::user user1; - user1.id = 189759562910400512; - user1.discriminator = 0001; - user1.username = "brain"; - - set_test(USER_GET_MENTION, false); - set_test(USER_GET_MENTION, user1.get_mention() == "<@189759562910400512>"); - - set_test(USER_FORMAT_USERNAME, false); - set_test(USER_FORMAT_USERNAME, user1.format_username() == "brain#0001"); - - set_test(USER_GET_CREATION_TIME, false); - set_test(USER_GET_CREATION_TIME, (uint64_t)user1.get_creation_time() == 1465312605); - - set_test(USER_GET_URL, false); - - dpp::user user2; - set_test(USER_GET_URL, - user1.get_url() == dpp::utility::url_host + "/users/189759562910400512" && - user2.get_url() == "" - ); - } - - { // avatar size function - set_test(UTILITY_AVATAR_SIZE, false); - bool success = false; - success = dpp::utility::avatar_size(0).empty(); - success = dpp::utility::avatar_size(16) == "?size=16" && success; - success = dpp::utility::avatar_size(256) == "?size=256" && success; - success = dpp::utility::avatar_size(4096) == "?size=4096" && success; - success = dpp::utility::avatar_size(8192).empty() && success; - success = dpp::utility::avatar_size(3000).empty() && success; - set_test(UTILITY_AVATAR_SIZE, success); - } - - { // cdn endpoint url getter - set_test(UTILITY_CDN_ENDPOINT_URL_HASH, false); - bool success = false; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_jpg, 0).empty(); - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 0) == "https://cdn.discordapp.com/foobar/test.png" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 128) == "https://cdn.discordapp.com/foobar/test.png?size=128" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_gif, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.png" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, false) == "https://cdn.discordapp.com/foobar/test/12345.png" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test.gif" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_gif, 0, false, false).empty() && success; - set_test(UTILITY_CDN_ENDPOINT_URL_HASH, success); - } - - { // sticker url getter - set_test(STICKER_GET_URL, false); - dpp::sticker s; - s.format_type = dpp::sf_png; - bool success = s.get_url().empty(); - s.id = 12345; - success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.png" && success; - s.format_type = dpp::sf_gif; - success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.gif" && success; - s.format_type = dpp::sf_lottie; - success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.json" && success; - set_test(STICKER_GET_URL, success); - } - - { // user url getter - dpp::user user1; - user1.id = 189759562910400512; - user1.username = "Brain"; - user1.discriminator = 0001; - - auto user2 = user1; - user2.avatar = "5532c6414c70765a28cf9448c117205f"; - - auto user3 = user2; - user3.flags |= dpp::u_animated_icon; - - set_test(USER_GET_AVATAR_URL, false); - set_test(USER_GET_AVATAR_URL, - dpp::user().get_avatar_url().empty() && - user1.get_avatar_url() == dpp::utility::cdn_host + "/embed/avatars/1.png" && - user2.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.png" && - user2.get_avatar_url(0, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.webp" && - user2.get_avatar_url(0, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.jpg" && - user3.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" && - user3.get_avatar_url(4096, dpp::i_gif) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=4096" && - user3.get_avatar_url(512, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && - user3.get_avatar_url(512, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && - user3.get_avatar_url(16, dpp::i_jpg, false) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.jpg?size=16" && - user3.get_avatar_url(5000) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" - ); - } - - { // emoji url getter - dpp::emoji emoji; - emoji.id = 825407338755653641; - - set_test(EMOJI_GET_URL, false); - set_test(EMOJI_GET_URL, emoji.get_url() == dpp::utility::cdn_host + "/emojis/825407338755653641.png"); - } - - { // message methods - dpp::message m; - m.guild_id = 825407338755653642; - m.channel_id = 956230231277072415; - m.id = 1151617986541666386; - - dpp::message m2; - m2.guild_id = 825407338755653642; - m2.channel_id = 956230231277072415; - - dpp::message m3; - m3.guild_id = 825407338755653642; - m3.id = 1151617986541666386; - - dpp::message m4; - m4.channel_id = 956230231277072415; - m4.id = 1151617986541666386; - - dpp::message m5; - m5.guild_id = 825407338755653642; - - dpp::message m6; - m6.channel_id = 956230231277072415; - - dpp::message m7; - m7.id = 1151617986541666386; - - dpp::message m8; - - set_test(MESSAGE_GET_URL, false); - set_test(MESSAGE_GET_URL, - m.get_url() == dpp::utility::url_host + "/channels/825407338755653642/956230231277072415/1151617986541666386" && - m2.get_url() == "" && - m3.get_url() == "" && - m4.get_url() == "" && - m5.get_url() == "" && - m6.get_url() == "" && - m7.get_url() == "" && - m8.get_url() == "" - ); - } - - { // channel methods - set_test(CHANNEL_SET_TYPE, false); - dpp::channel c; - c.set_flags(dpp::c_nsfw | dpp::c_video_quality_720p); - c.set_type(dpp::CHANNEL_CATEGORY); - bool before = c.is_category() && !c.is_forum(); - c.set_type(dpp::CHANNEL_FORUM); - bool after = !c.is_category() && c.is_forum(); - set_test(CHANNEL_SET_TYPE, before && after); - - set_test(CHANNEL_GET_MENTION, false); - c.id = 825411707521728511; - set_test(CHANNEL_GET_MENTION, c.get_mention() == "<#825411707521728511>"); - - set_test(CHANNEL_GET_URL, false); - c.guild_id = 825407338755653642; - - dpp::channel c2; - c2.id = 825411707521728511; - - dpp::channel c3; - c3.guild_id = 825407338755653642; - - dpp::channel c4; - - set_test(CHANNEL_GET_URL, - c.get_url() == dpp::utility::url_host + "/channels/825407338755653642/825411707521728511" && - c2.get_url() == "" && - c3.get_url() == "" && - c4.get_url() == "" - ); - } - - { // utility methods - set_test(UTILITY_GUILD_NAVIGATION, false); - auto gn1 = dpp::utility::guild_navigation(123, dpp::utility::gnt_customize); - auto gn2 = dpp::utility::guild_navigation(1234, dpp::utility::gnt_browse); - auto gn3 = dpp::utility::guild_navigation(12345, dpp::utility::gnt_guide); - set_test(UTILITY_GUILD_NAVIGATION, gn1 == "<123:customize>" && gn2 == "<1234:browse>" && gn3 == "<12345:guide>"); - - set_test(UTILITY_ICONHASH, false); - auto iconhash1 = dpp::utility::iconhash("a_5532c6414c70765a28cf9448c117205f"); - set_test(UTILITY_ICONHASH, iconhash1.first == 6139187225817019994 && - iconhash1.second == 2940732121894297695 && - iconhash1.to_string() == "5532c6414c70765a28cf9448c117205f" - ); - - set_test(UTILITY_MAKE_URL_PARAMETERS, false); - auto url_params1 = dpp::utility::make_url_parameters({ - {"foo", 15}, - {"bar", 7} - }); - auto url_params2 = dpp::utility::make_url_parameters({ - {"foo", "hello"}, - {"bar", "two words"} - }); - set_test(UTILITY_MAKE_URL_PARAMETERS, url_params1 == "?bar=7&foo=15" && url_params2 == "?bar=two%20words&foo=hello"); - - set_test(UTILITY_MARKDOWN_ESCAPE, false); - auto escaped = dpp::utility::markdown_escape( - "> this is a quote\n" - "**some bold text**"); - set_test(UTILITY_MARKDOWN_ESCAPE, "\\>this is a quote\\n\\*\\*some bold text\\*\\*"); - - set_test(UTILITY_TOKENIZE, false); - auto tokens = dpp::utility::tokenize("some Whitespace seperated Text to Tokenize", " "); - std::vector expected_tokens = {"some", "Whitespace", "seperated", "Text", "to", "Tokenize"}; - set_test(UTILITY_TOKENIZE, tokens == expected_tokens); - - set_test(UTILITY_URL_ENCODE, false); - auto url_encoded = dpp::utility::url_encode("S2-^$1Nd+U!g'8+_??o?p-bla bla"); - set_test(UTILITY_URL_ENCODE, url_encoded == "S2-%5E%241Nd%2BU%21g%278%2B_%3F%3Fo%3Fp-bla%20bla"); - - set_test(UTILITY_SLASHCOMMAND_MENTION, false); - auto mention1 = dpp::utility::slashcommand_mention(123, "name"); - auto mention2 = dpp::utility::slashcommand_mention(123, "name", "sub"); - auto mention3 = dpp::utility::slashcommand_mention(123, "name", "group", "sub"); - bool success = mention1 == "" && mention2 == "" && mention3 == ""; - set_test(UTILITY_SLASHCOMMAND_MENTION, success); - - set_test(UTILITY_CHANNEL_MENTION, false); - auto channel_mention = dpp::utility::channel_mention(123); - set_test(UTILITY_CHANNEL_MENTION, channel_mention == "<#123>"); - - set_test(UTILITY_USER_MENTION, false); - auto user_mention = dpp::utility::user_mention(123); - set_test(UTILITY_USER_MENTION, user_mention == "<@123>"); - - set_test(UTILITY_ROLE_MENTION, false); - auto role_mention = dpp::utility::role_mention(123); - set_test(UTILITY_ROLE_MENTION, role_mention == "<@&123>"); - - set_test(UTILITY_EMOJI_MENTION, false); - auto emoji_mention1 = dpp::utility::emoji_mention("role1", 123, false); - auto emoji_mention2 = dpp::utility::emoji_mention("role2", 234, true); - auto emoji_mention3 = dpp::utility::emoji_mention("white_check_mark", 0, false); - auto emoji_mention4 = dpp::utility::emoji_mention("white_check_mark", 0, true); - set_test(UTILITY_EMOJI_MENTION, - emoji_mention1 == "<:role1:123>" && - emoji_mention2 == "" && - emoji_mention3 == ":white_check_mark:" && - emoji_mention4 == ":white_check_mark:" - ); - - set_test(UTILITY_USER_URL, false); - auto user_url = dpp::utility::user_url(123); - set_test(UTILITY_USER_URL, - user_url == dpp::utility::url_host + "/users/123" && - dpp::utility::user_url(0) == "" - ); - - set_test(UTILITY_MESSAGE_URL, false); - auto message_url = dpp::utility::message_url(1,2,3); - set_test(UTILITY_MESSAGE_URL, - message_url == dpp::utility::url_host+ "/channels/1/2/3" && - dpp::utility::message_url(0,2,3) == "" && - dpp::utility::message_url(1,0,3) == "" && - dpp::utility::message_url(1,2,0) == "" && - dpp::utility::message_url(0,0,3) == "" && - dpp::utility::message_url(0,2,0) == "" && - dpp::utility::message_url(1,0,0) == "" && - dpp::utility::message_url(0,0,0) == "" - ); - - set_test(UTILITY_CHANNEL_URL, false); - auto channel_url = dpp::utility::channel_url(1,2); - set_test(UTILITY_CHANNEL_URL, - channel_url == dpp::utility::url_host+ "/channels/1/2" && - dpp::utility::channel_url(0,2) == "" && - dpp::utility::channel_url(1,0) == "" && - dpp::utility::channel_url(0,0) == "" - ); - - set_test(UTILITY_THREAD_URL, false); - auto thread_url = dpp::utility::thread_url(1,2); - set_test(UTILITY_THREAD_URL, - thread_url == dpp::utility::url_host+ "/channels/1/2" && - dpp::utility::thread_url(0,2) == "" && - dpp::utility::thread_url(1,0) == "" && - dpp::utility::thread_url(0,0) == "" - ); - } - -#ifndef _WIN32 - set_test(TIMESTRINGTOTIMESTAMP, false); - json tj; - tj["t1"] = "2022-01-19T17:18:14.506000+00:00"; - tj["t2"] = "2022-01-19T17:18:14+00:00"; - uint32_t inTimestamp = 1642612694; - set_test(TIMESTRINGTOTIMESTAMP, (uint64_t)dpp::ts_not_null(&tj, "t1") == inTimestamp && (uint64_t)dpp::ts_not_null(&tj, "t2") == inTimestamp); -#else - set_test(TIMESTRINGTOTIMESTAMP, true); -#endif + dpp::cluster bot(token, dpp::i_all_intents); - { - set_test(TS, false); - dpp::managed m(189759562910400512); - set_test(TS, ((uint64_t) m.get_creation_time()) == 1465312605); - } + errors_test(); + http_client_tests(token); + discord_objects_tests(); + gateway_events_tests(token, bot); + cache_tests(bot); + utility_tests(); { coro_offline_tests(); } - std::vector test_image = load_test_image(); - - set_test(PRESENCE, false); - set_test(CLUSTER, false); - try { - dpp::cluster bot(token, dpp::i_all_intents); - bot.set_websocket_protocol(dpp::ws_etf); - set_test(CLUSTER, true); - set_test(CONNECTION, false); - set_test(GUILDCREATE, false); - set_test(ICONHASH, false); - - set_test(MSGCOLLECT, false); - if (!offline) { - /* Intentional leak: freed on unit test end */ - [[maybe_unused]] - message_collector* collect_messages = new message_collector(&bot, 25); - } - - set_test(JSON_PARSE_ERROR, false); - dpp::rest_request(&bot, "/nonexistent", "address", "", dpp::m_get, "", [](const dpp::confirmation_callback_t& e) { - if (e.is_error() && e.get_error().code == 404) { - set_test(JSON_PARSE_ERROR, true); - } else { - set_test(JSON_PARSE_ERROR, false); - } - }); - - dpp::utility::iconhash i; - std::string dummyval("fcffffffffffff55acaaaaaaaaaaaa66"); - i = dummyval; - set_test(ICONHASH, (i.to_string() == dummyval)); - - /* This ensures we test both protocols, as voice is json and shard is etf */ - bot.set_websocket_protocol(dpp::ws_etf); - - bot.on_form_submit([&](const dpp::form_submit_t & event) { - }); - - /* This is near impossible to test without a 'clean room' voice channel. - * We attach this event just so that the decoder events are fired while we - * are sending audio later, this way if the audio receive code is plain unstable - * the test suite will crash and fail. - */ - bot.on_voice_receive_combined([&](const auto& event) { - }); - - std::promise ready_promise; - std::future ready_future = ready_promise.get_future(); - bot.on_ready([&](const dpp::ready_t & event) { - set_test(CONNECTION, true); - ready_promise.set_value(); - - set_test(APPCOMMAND, false); - set_test(LOGGER, false); - bot.log(dpp::ll_info, "Test log message"); - - bot.guild_command_create(dpp::slashcommand().set_name("testcommand") - .set_description("Test command for DPP unit test") - .add_option(dpp::command_option(dpp::co_attachment, "file", "a file")) - .set_application_id(bot.me.id) - .add_localization("fr", "zut", "Ou est la salor dans Discord?"), - TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(APPCOMMAND, true); - set_test(DELCOMMAND, false); - dpp::slashcommand s = std::get(callback.value); - bot.guild_command_delete(s.id, TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - dpp::message test_message(TEST_TEXT_CHANNEL_ID, "test message"); - - set_test(DELCOMMAND, true); - set_test(MESSAGECREATE, false); - set_test(MESSAGEEDIT, false); - set_test(MESSAGERECEIVE, false); - test_message.add_file("no-mime", "test"); - test_message.add_file("test.txt", "test", "text/plain"); - test_message.add_file("test.png", std::string{test_image.begin(), test_image.end()}, "image/png"); - bot.message_create(test_message, [&bot](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(MESSAGECREATE, true); - set_test(REACT, false); - dpp::message m = std::get(callback.value); - set_test(REACTEVENT, false); - bot.message_add_reaction(m.id, TEST_TEXT_CHANNEL_ID, "😄", [](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(REACT, true); - } else { - set_test(REACT, false); - } - }); - set_test(EDITEVENT, false); - bot.message_edit(dpp::message(m).set_content("test edit"), [](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(MESSAGEEDIT, true); - } - }); - } - }); - } else { - set_test(DELCOMMAND, false); - } - }); - } - }); - }); - - std::mutex loglock; - bot.on_log([&](const dpp::log_t & event) { - std::lock_guard locker(loglock); - if (event.severity > dpp::ll_trace) { - std::cout << "[" << std::fixed << std::setprecision(3) << (dpp::utility::time_f() - get_start_time()) << "]: [\u001b[36m" << dpp::utility::loglevel(event.severity) << "\u001b[0m] " << event.message << "\n"; - } - if (event.message == "Test log message") { - set_test(LOGGER, true); - } - }); - - set_test(RUNONCE, false); - uint8_t runs = 0; - for (int x = 0; x < 10; ++x) { - if (dpp::run_once()) { - runs++; - } - } - set_test(RUNONCE, (runs == 1)); - - bot.on_voice_ready([&](const dpp::voice_ready_t & event) { - set_test(VOICECONN, true); - dpp::discord_voice_client* v = event.voice_client; - set_test(VOICESEND, false); - if (v && v->is_ready()) { - v->send_audio_raw((uint16_t*)testaudio.data(), testaudio.size()); - } else { - set_test(VOICESEND, false); - } - }); - - bot.on_invite_create([](const dpp::invite_create_t &event) { - auto &inv = event.created_invite; - if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID && inv.created_at != 0 && inv.max_uses == 100) { - set_test(INVITE_CREATE_EVENT, true); - } - }); - - bot.on_invite_delete([](const dpp::invite_delete_t &event) { - auto &inv = event.deleted_invite; - if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID) { - set_test(INVITE_DELETE_EVENT, true); - } - }); - - bot.on_voice_buffer_send([&](const dpp::voice_buffer_send_t & event) { - if (event.buffer_size == 0) { - set_test(VOICESEND, true); - } - }); - - set_test(SYNC, false); - if (!offline) { - dpp::message m = dpp::sync(&bot, &dpp::cluster::message_create, dpp::message(TEST_TEXT_CHANNEL_ID, "TEST")); - set_test(SYNC, m.content == "TEST"); - } - - bot.on_guild_create([&](const dpp::guild_create_t & event) { - if (event.created->id == TEST_GUILD_ID) { - set_test(GUILDCREATE, true); - if (event.presences.size() && event.presences.begin()->second.user_id > 0) { - set_test(PRESENCE, true); - } - dpp::guild* g = dpp::find_guild(TEST_GUILD_ID); - set_test(CACHE, false); - if (g) { - set_test(CACHE, true); - set_test(VOICECONN, false); - dpp::discord_client* s = bot.get_shard(0); - s->connect_voice(g->id, TEST_VC_ID, false, false); - } - else { - set_test(CACHE, false); - } - } - }); - - // this helper class contains logic for the message tests, deletes the message when all tests are done - class message_test_helper - { - private: - std::mutex mutex; - bool pin_tested; - bool thread_tested; - std::array files_tested; - std::array files_success; - dpp::snowflake channel_id; - dpp::snowflake message_id; - dpp::cluster ⊥ - - void delete_message_if_done() { - if (files_tested == std::array{true, true, true} && pin_tested && thread_tested) { - set_test(MESSAGEDELETE, false); - bot.message_delete(message_id, channel_id, [](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(MESSAGEDELETE, true); - } - }); - } - } - - void set_pin_tested() { - assert(!pin_tested); - pin_tested = true; - delete_message_if_done(); - } - - void set_thread_tested() { - assert(!thread_tested); - thread_tested = true; - delete_message_if_done(); - } - - void set_file_tested(size_t index) { - assert(!files_tested[index]); - files_tested[index] = true; - if (files_tested == std::array{true, true, true}) { - set_test(MESSAGEFILE, files_success == std::array{true, true, true}); - } - delete_message_if_done(); - } - - void test_threads(const dpp::message &message) - { - set_test(THREAD_CREATE_MESSAGE, false); - set_test(THREAD_DELETE, false); - set_test(THREAD_DELETE_EVENT, false); - bot.thread_create_with_message("test", message.channel_id, message.id, 60, 60, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (callback.is_error()) { - set_thread_tested(); - } - else { - auto thread = callback.get(); - thread_id = thread.id; - set_test(THREAD_CREATE_MESSAGE, true); - bot.channel_delete(thread.id, [this](const dpp::confirmation_callback_t &callback) { - set_test(THREAD_DELETE, !callback.is_error()); - set_thread_tested(); - }); - } - }); - } - - void test_files(const dpp::message &message) { - set_test(MESSAGEFILE, false); - if (message.attachments.size() == 3) { - static constexpr auto check_mimetype = [](const auto &headers, std::string mimetype) { - if (auto it = headers.find("content-type"); it != headers.end()) { - // check that the mime type starts with what we gave : for example discord will change "text/plain" to "text/plain; charset=UTF-8" - return it->second.size() >= mimetype.size() && std::equal(it->second.begin(), it->second.begin() + mimetype.size(), mimetype.begin()); - } - else { - return false; - } - }; - message.attachments[0].download([&](const dpp::http_request_completion_t &callback) { - std::lock_guard lock(mutex); - if (callback.status == 200 && callback.body == "test") { - files_success[0] = true; - } - set_file_tested(0); - }); - message.attachments[1].download([&](const dpp::http_request_completion_t &callback) { - std::lock_guard lock(mutex); - if (callback.status == 200 && check_mimetype(callback.headers, "text/plain") && callback.body == "test") { - files_success[1] = true; - } - set_file_tested(1); - }); - message.attachments[2].download([&](const dpp::http_request_completion_t &callback) { - std::lock_guard lock(mutex); - // do not check the contents here because discord can change compression - if (callback.status == 200 && check_mimetype(callback.headers, "image/png")) { - files_success[2] = true; - } - set_file_tested(2); - }); - } - else { - set_file_tested(0); - set_file_tested(1); - set_file_tested(2); - } - } - - void test_pin() { - if (!extended) { - set_pin_tested(); - return; - } - set_test(MESSAGEPIN, false); - set_test(MESSAGEUNPIN, false); - bot.message_pin(channel_id, message_id, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (!callback.is_error()) { - set_test(MESSAGEPIN, true); - bot.message_unpin(TEST_TEXT_CHANNEL_ID, message_id, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (!callback.is_error()) { - set_test(MESSAGEUNPIN, true); - } - set_pin_tested(); - }); - } - else { - set_pin_tested(); - } - }); - } - - public: - dpp::snowflake thread_id; - - message_test_helper(dpp::cluster &_bot) : bot(_bot) {} - - void run(const dpp::message &message) { - pin_tested = false; - thread_tested = false; - files_tested = {false, false, false}; - files_success = {false, false, false}; - channel_id = message.channel_id; - message_id = message.id; - test_pin(); - test_files(message); - test_threads(message); - } - }; - - message_test_helper message_helper(bot); - - class thread_test_helper - { - public: - enum event_flag - { - MESSAGE_CREATE = 1 << 0, - MESSAGE_EDIT = 1 << 1, - MESSAGE_REACT = 1 << 2, - MESSAGE_REMOVE_REACT = 1 << 3, - MESSAGE_DELETE = 1 << 4, - EVENT_END = 1 << 5 - }; - private: - std::mutex mutex; - dpp::cluster ⊥ - bool edit_tested = false; - bool members_tested = false; - bool messages_tested = false; - bool events_tested = false; - bool get_active_tested = false; - uint32_t events_tested_mask = 0; - uint32_t events_to_test_mask = 0; - - void delete_if_done() - { - if (edit_tested && members_tested && messages_tested && events_tested && get_active_tested) { - bot.channel_delete(thread_id); - } - } - - void set_events_tested() - { - if (events_tested) { - return; - } - events_tested = true; - delete_if_done(); - } - - void set_edit_tested() - { - if (edit_tested) { - return; - } - edit_tested = true; - delete_if_done(); - } - - void set_members_tested() - { - if (members_tested) { - return; - } - members_tested = true; - delete_if_done(); - } - - void set_get_active_tested() - { - if (get_active_tested) { - return; - } - get_active_tested = true; - delete_if_done(); - } - - void set_messages_tested() - { - if (messages_tested) { - return; - } - messages_tested = true; - delete_if_done(); - } - - void set_event_tested(event_flag flag) - { - if (events_tested_mask & flag) { - return; - } - events_tested_mask |= flag; - for (uint32_t i = 1; i < EVENT_END; i <<= 1) { - if ((events_to_test_mask & i) && (events_tested_mask & i) != i) { - return; - } - } - set_events_tested(); - } - - void events_abort() - { - events_tested_mask |= ~events_to_test_mask; - for (uint32_t i = 1; i < EVENT_END; i <<= 1) { - if ((events_tested_mask & i) != i) { - return; - } - } - set_events_tested(); - } - - public: - /** - * @Brief wrapper for set_event_tested, locking the mutex. Meant to be used from outside the class - */ - void notify_event_tested(event_flag flag) - { - std::lock_guard lock{mutex}; - - set_event_tested(flag); - } - - dpp::snowflake thread_id; - - void test_edit(const dpp::thread &thread) - { - std::lock_guard lock{mutex}; - - if (!edit_tested) { - dpp::thread edit = thread; - set_test(THREAD_EDIT, false); - set_test(THREAD_UPDATE_EVENT, false); - edit.name = "edited"; - edit.metadata.locked = true; - bot.thread_edit(edit, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (!callback.is_error()) { - set_test(THREAD_EDIT, true); - } - set_edit_tested(); - }); - } - } - - void test_get_active(const dpp::thread &thread) - { - std::lock_guard lock{mutex}; - - set_test(THREAD_GET_ACTIVE, false); - bot.threads_get_active(TEST_GUILD_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (!callback.is_error()) { - const auto &threads = callback.get(); - if (auto thread_it = threads.find(thread_id); thread_it != threads.end()) { - const auto &thread = thread_it->second.active_thread; - const auto &member = thread_it->second.bot_member; - if (thread.id == thread_id && member.has_value() && member->user_id == bot.me.id) { - set_test(THREAD_GET_ACTIVE, true); - } - } - } - set_get_active_tested(); - }); - } - - void test_members(const dpp::thread &thread) - { - std::lock_guard lock{mutex}; - - if (!members_tested) { - if (!extended) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBER_ADD, false); - set_test(THREAD_MEMBER_GET, false); - set_test(THREAD_MEMBERS_GET, false); - set_test(THREAD_MEMBER_REMOVE, false); - set_test(THREAD_MEMBERS_ADD_EVENT, false); - set_test(THREAD_MEMBERS_REMOVE_EVENT, false); - bot.thread_member_add(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBER_ADD, true); - bot.thread_member_get(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBER_GET, true); - bot.thread_members_get(thread_id, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - set_members_tested(); - return; - } - const auto &members = callback.get(); - if (members.find(TEST_USER_ID) == members.end() || members.find(bot.me.id) == members.end()) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBERS_GET, true); - bot.thread_member_remove(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (!callback.is_error()) { - set_test(THREAD_MEMBER_REMOVE, true); - } - set_members_tested(); - }); - }); - }); - }); - } - } - - void test_messages(const dpp::thread &thread) - { - if (!extended) { - set_messages_tested(); - set_events_tested(); - return; - } - - std::lock_guard lock{mutex}; - set_test(THREAD_MESSAGE, false); - set_test(THREAD_MESSAGE_CREATE_EVENT, false); - set_test(THREAD_MESSAGE_EDIT_EVENT, false); - set_test(THREAD_MESSAGE_REACT_ADD_EVENT, false); - set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, false); - set_test(THREAD_MESSAGE_DELETE_EVENT, false); - events_to_test_mask |= MESSAGE_CREATE; - bot.message_create(dpp::message{"hello thread"}.set_channel_id(thread.id), [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - auto m = callback.get(); - m.content = "hello thread?"; - events_to_test_mask |= MESSAGE_EDIT; - bot.message_edit(m, [this, message_id = m.id](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - events_to_test_mask |= MESSAGE_REACT; - bot.message_add_reaction(message_id, thread_id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - events_to_test_mask |= MESSAGE_REMOVE_REACT; - bot.message_delete_reaction(message_id, thread_id, bot.me.id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - events_to_test_mask |= MESSAGE_DELETE; - bot.message_delete(message_id, thread_id, [this] (const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - set_messages_tested(); - if (callback.is_error()) { - events_abort(); - return; - } - set_test(THREAD_MESSAGE, true); - }); - }); - }); - }); - }); - } - - void run(const dpp::thread &thread) - { - thread_id = thread.id; - test_get_active(thread); - test_edit(thread); - test_members(thread); - test_messages(thread); - } - - thread_test_helper(dpp::cluster &bot_) : bot{bot_} - { - } - }; - - thread_test_helper thread_helper(bot); - - bot.on_thread_create([&](const dpp::thread_create_t &event) { - if (event.created.name == "thread test") { - set_test(THREAD_CREATE_EVENT, true); - thread_helper.run(event.created); - } - }); - - bool message_tested = false; - bot.on_message_create([&](const dpp::message_create_t & event) { - if (event.msg.author.id == bot.me.id) { - if (event.msg.content == "test message" && !message_tested) { - message_tested = true; - set_test(MESSAGERECEIVE, true); - message_helper.run(event.msg); - set_test(MESSAGESGET, false); - bot.messages_get(event.msg.channel_id, 0, event.msg.id, 0, 5, [](const dpp::confirmation_callback_t &cc){ - if (!cc.is_error()) { - dpp::message_map mm = std::get(cc.value); - if (mm.size()) { - set_test(MESSAGESGET, true); - set_test(TIMESTAMP, false); - dpp::message m = mm.begin()->second; - if (m.sent > 0) { - set_test(TIMESTAMP, true); - } else { - set_test(TIMESTAMP, false); - } - } else { - set_test(MESSAGESGET, false); - } - } else { - set_test(MESSAGESGET, false); - } - }); - set_test(MSGCREATESEND, false); - event.send("MSGCREATESEND", [&bot, ch_id = event.msg.channel_id] (const auto& cc) { - if (!cc.is_error()) { - dpp::message m = std::get(cc.value); - if (m.channel_id == ch_id) { - set_test(MSGCREATESEND, true); - } else { - bot.log(dpp::ll_debug, cc.http_info.body); - set_test(MSGCREATESEND, false); - } - bot.message_delete(m.id, m.channel_id); - } else { - bot.log(dpp::ll_debug, cc.http_info.body); - set_test(MSGCREATESEND, false); - } - }); - } - if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread") { - set_test(THREAD_MESSAGE_CREATE_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_CREATE); - } - } - }); - - bot.on_message_reaction_add([&](const dpp::message_reaction_add_t & event) { - if (event.reacting_user.id == bot.me.id) { - if (event.reacting_emoji.name == "😄") { - set_test(REACTEVENT, true); - } - if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { - set_test(THREAD_MESSAGE_REACT_ADD_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REACT); - } - } - }); - - bot.on_message_reaction_remove([&](const dpp::message_reaction_remove_t & event) { - if (event.reacting_user_id == bot.me.id) { - if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { - set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REMOVE_REACT); - } - } - }); - - bot.on_message_delete([&](const dpp::message_delete_t & event) { - if (event.channel_id == thread_helper.thread_id) { - set_test(THREAD_MESSAGE_DELETE_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_DELETE); - } - }); - - bool message_edit_tested = false; - bot.on_message_update([&](const dpp::message_update_t &event) { - if (event.msg.author == bot.me.id) { - if (event.msg.content == "test edit" && !message_edit_tested) { - message_edit_tested = true; - set_test(EDITEVENT, true); - } - if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread?") { - set_test(THREAD_MESSAGE_EDIT_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_EDIT); - } - } - }); - - bot.on_thread_update([&](const dpp::thread_update_t &event) { - if (event.updating_guild->id == TEST_GUILD_ID && event.updated.id == thread_helper.thread_id && event.updated.name == "edited") { - set_test(THREAD_UPDATE_EVENT, true); - } - }); - - bot.on_thread_members_update([&](const dpp::thread_members_update_t &event) { - if (event.updating_guild->id == TEST_GUILD_ID && event.thread_id == thread_helper.thread_id) { - if (std::find_if(std::begin(event.added), std::end(event.added), is_owner) != std::end(event.added)) { - set_test(THREAD_MEMBERS_ADD_EVENT, true); - } - if (std::find_if(std::begin(event.removed_ids), std::end(event.removed_ids), is_owner) != std::end(event.removed_ids)) { - set_test(THREAD_MEMBERS_REMOVE_EVENT, true); - } - } - }); - - bot.on_thread_delete([&](const dpp::thread_delete_t &event) { - if (event.deleting_guild->id == TEST_GUILD_ID && event.deleted.id == message_helper.thread_id) { - set_test(THREAD_DELETE_EVENT, true); - } - }); - - // set to execute from this thread (main thread) after on_ready is fired - auto do_online_tests = [&] { - coro_online_tests(&bot); - set_test(GUILD_BAN_CREATE, false); - set_test(GUILD_BAN_GET, false); - set_test(GUILD_BANS_GET, false); - set_test(GUILD_BAN_DELETE, false); - if (!offline) { - // some deleted discord accounts to test the ban stuff with... - dpp::snowflake deadUser1(802670069523415057); - dpp::snowflake deadUser2(875302419335094292); - dpp::snowflake deadUser3(1048247361903792198); - - bot.set_audit_reason("ban reason one").guild_ban_add(TEST_GUILD_ID, deadUser1, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) bot.guild_ban_add(TEST_GUILD_ID, deadUser2, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) bot.set_audit_reason("ban reason three").guild_ban_add(TEST_GUILD_ID, deadUser3, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - set_test(GUILD_BAN_CREATE, true); - // when created, continue with getting and deleting - - // get ban - bot.guild_get_ban(TEST_GUILD_ID, deadUser1, [deadUser1](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - dpp::ban ban = event.get(); - if (ban.user_id == deadUser1 && ban.reason == "ban reason one") { - set_test(GUILD_BAN_GET, true); - } - } - }); - - // get multiple bans - bot.guild_get_bans(TEST_GUILD_ID, 0, deadUser1, 3, [deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - dpp::ban_map bans = event.get(); - int successCount = 0; - for (auto &ban: bans) { - if (ban.first == ban.second.user_id) { // the key should match the ban's user_id - if (ban.first == deadUser2 && ban.second.reason.empty()) { - successCount++; - } else if (ban.first == deadUser3 && ban.second.reason == "ban reason three") { - successCount++; - } - } - } - if (successCount == 2) { - set_test(GUILD_BANS_GET, true); - } - } - }); - - // unban them - bot.guild_ban_delete(TEST_GUILD_ID, deadUser1, [&bot, deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - bot.guild_ban_delete(TEST_GUILD_ID, deadUser2, [&bot, deadUser3](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - bot.guild_ban_delete(TEST_GUILD_ID, deadUser3, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(GUILD_BAN_DELETE, true); - } - }); - } - }); - } - }); - }); - }); - }); - } - - set_test(REQUEST_GET_IMAGE, false); - if (!offline) { - bot.request("https://dpp.dev/DPP-Logo.png", dpp::m_get, [&bot](const dpp::http_request_completion_t &callback) { - if (callback.status != 200) { - return; - } - set_test(REQUEST_GET_IMAGE, true); - - dpp::emoji emoji; - emoji.load_image(callback.body, dpp::i_png); - emoji.name = "dpp"; - - // emoji unit test with the requested image - set_test(EMOJI_CREATE, false); - set_test(EMOJI_GET, false); - set_test(EMOJI_DELETE, false); - bot.guild_emoji_create(TEST_GUILD_ID, emoji, [&bot](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - set_test(EMOJI_CREATE, true); - - auto created = event.get(); - bot.guild_emoji_get(TEST_GUILD_ID, created.id, [&bot, created](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto fetched = event.get(); - if (created.id == fetched.id && created.name == fetched.name && created.flags == fetched.flags) { - set_test(EMOJI_GET, true); - } - - bot.guild_emoji_delete(TEST_GUILD_ID, fetched.id, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(EMOJI_DELETE, true); - } - }); - }); - }); - }); - } - - set_test(INVITE_CREATE, false); - set_test(INVITE_GET, false); - set_test(INVITE_DELETE, false); - if (!offline) { - dpp::channel channel; - channel.id = TEST_TEXT_CHANNEL_ID; - dpp::invite invite; - invite.max_age = 0; - invite.max_uses = 100; - set_test(INVITE_CREATE_EVENT, false); - bot.channel_invite_create(channel, invite, [&bot, invite](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - - auto created = event.get(); - if (!created.code.empty() && created.channel_id == TEST_TEXT_CHANNEL_ID && created.guild_id == TEST_GUILD_ID && created.inviter.id == bot.me.id) { - set_test(INVITE_CREATE, true); - } - - bot.invite_get(created.code, [&bot, created](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - auto retrieved = event.get(); - if (retrieved.code == created.code && retrieved.guild_id == created.guild_id && retrieved.channel_id == created.channel_id && retrieved.inviter.id == created.inviter.id) { - if (retrieved.destination_guild.flags & dpp::g_community) { - set_test(INVITE_GET, retrieved.expires_at == 0); - } else { - set_test(INVITE_GET, true); - } - - } else { - set_test(INVITE_GET, false); - } - } else { - set_test(INVITE_GET, false); - } - - set_test(INVITE_DELETE_EVENT, false); - bot.invite_delete(created.code, [](const dpp::confirmation_callback_t &event) { - set_test(INVITE_DELETE, !event.is_error()); - }); - }); - }); - } - - set_test(AUTOMOD_RULE_CREATE, false); - set_test(AUTOMOD_RULE_GET, false); - set_test(AUTOMOD_RULE_GET_ALL, false); - set_test(AUTOMOD_RULE_DELETE, false); - if (!offline) { - dpp::automod_rule automodRule; - automodRule.name = "automod rule (keyword type)"; - automodRule.trigger_type = dpp::amod_type_keyword; - dpp::automod_metadata metadata1; - metadata1.keywords.emplace_back("*cat*"); - metadata1.keywords.emplace_back("train"); - metadata1.keywords.emplace_back("*.exe"); - metadata1.regex_patterns.emplace_back("^[^a-z]$"); - metadata1.allow_list.emplace_back("@silent*"); - automodRule.trigger_metadata = metadata1; - dpp::automod_action automodAction; - automodAction.type = dpp::amod_action_timeout; - automodAction.duration_seconds = 6000; - automodRule.actions.emplace_back(automodAction); - - bot.automod_rules_get(TEST_GUILD_ID, [&bot, automodRule](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto rules = event.get(); - set_test(AUTOMOD_RULE_GET_ALL, true); - for (const auto &rule: rules) { - if (rule.second.trigger_type == dpp::amod_type_keyword) { - // delete one automod rule of type KEYWORD before creating one to make space... - bot.automod_rule_delete(TEST_GUILD_ID, rule.first); - } - } - - // start creating the automod rules - bot.automod_rule_create(TEST_GUILD_ID, automodRule, [&bot, automodRule](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto created = event.get(); - if (created.name == automodRule.name) { - set_test(AUTOMOD_RULE_CREATE, true); - } - - // get automod rule - bot.automod_rule_get(TEST_GUILD_ID, created.id, [automodRule, &bot, created](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto retrieved = event.get(); - if (retrieved.name == automodRule.name && - retrieved.trigger_type == automodRule.trigger_type && - retrieved.trigger_metadata.keywords == automodRule.trigger_metadata.keywords && - retrieved.trigger_metadata.regex_patterns == automodRule.trigger_metadata.regex_patterns && - retrieved.trigger_metadata.allow_list == automodRule.trigger_metadata.allow_list && retrieved.actions.size() == automodRule.actions.size()) { - set_test(AUTOMOD_RULE_GET, true); - } - - // delete the automod rule - bot.automod_rule_delete(TEST_GUILD_ID, retrieved.id, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(AUTOMOD_RULE_DELETE, true); - } - }); - }); - }); - }); - } - - set_test(USER_GET, false); - set_test(USER_GET_FLAGS, false); - if (!offline) { - bot.user_get(TEST_USER_ID, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - auto u = std::get(event.value); - if (u.id == TEST_USER_ID) { - set_test(USER_GET, true); - } else { - set_test(USER_GET, false); - } - json j = json::parse(event.http_info.body); - uint64_t raw_flags = j["public_flags"]; - if (j.contains("flags")) { - uint64_t flags = j["flags"]; - raw_flags |= flags; - } - // testing all user flags from https://discord.com/developers/docs/resources/user#user-object-user-flags - // they're manually set here because the dpp::user_flags don't match to the discord API, so we can't use them to compare with the raw flags! - if ( - u.is_discord_employee() == ((raw_flags & (1 << 0)) != 0) && - u.is_partnered_owner() == ((raw_flags & (1 << 1)) != 0) && - u.has_hypesquad_events() == ((raw_flags & (1 << 2)) != 0) && - u.is_bughunter_1() == ((raw_flags & (1 << 3)) != 0) && - u.is_house_bravery() == ((raw_flags & (1 << 6)) != 0) && - u.is_house_brilliance() == ((raw_flags & (1 << 7)) != 0) && - u.is_house_balance() == ((raw_flags & (1 << 8)) != 0) && - u.is_early_supporter() == ((raw_flags & (1 << 9)) != 0) && - u.is_team_user() == ((raw_flags & (1 << 10)) != 0) && - u.is_bughunter_2() == ((raw_flags & (1 << 14)) != 0) && - u.is_verified_bot() == ((raw_flags & (1 << 16)) != 0) && - u.is_verified_bot_dev() == ((raw_flags & (1 << 17)) != 0) && - u.is_certified_moderator() == ((raw_flags & (1 << 18)) != 0) && - u.is_bot_http_interactions() == ((raw_flags & (1 << 19)) != 0) && - u.is_active_developer() == ((raw_flags & (1 << 22)) != 0) - ) { - set_test(USER_GET_FLAGS, true); - } else { - set_test(USER_GET_FLAGS, false); - } - } else { - set_test(USER_GET, false); - set_test(USER_GET_FLAGS, false); - } - }); - } - - set_test(VOICE_CHANNEL_CREATE, false); - set_test(VOICE_CHANNEL_EDIT, false); - set_test(VOICE_CHANNEL_DELETE, false); - if (!offline) { - dpp::channel channel1; - channel1.set_type(dpp::CHANNEL_VOICE) - .set_guild_id(TEST_GUILD_ID) - .set_name("voice1") - .add_permission_overwrite(TEST_GUILD_ID, dpp::ot_role, 0, dpp::p_view_channel) - .set_user_limit(99); - dpp::channel createdChannel; - try { - createdChannel = bot.channel_create_sync(channel1); - } catch (dpp::rest_exception &exception) { - set_test(VOICE_CHANNEL_CREATE, false); - } - if (createdChannel.name == channel1.name && - createdChannel.user_limit == 99 && - createdChannel.name == "voice1") { - for (auto overwrite: createdChannel.permission_overwrites) { - if (overwrite.id == TEST_GUILD_ID && overwrite.type == dpp::ot_role && overwrite.deny == dpp::p_view_channel) { - set_test(VOICE_CHANNEL_CREATE, true); - } - } - - // edit the voice channel - createdChannel.set_name("foobar2"); - createdChannel.set_user_limit(2); - for (auto overwrite: createdChannel.permission_overwrites) { - if (overwrite.id == TEST_GUILD_ID) { - overwrite.deny.set(0); - overwrite.allow.set(dpp::p_view_channel); - } - } - try { - dpp::channel edited = bot.channel_edit_sync(createdChannel); - if (edited.name == "foobar2" && edited.user_limit == 2) { - set_test(VOICE_CHANNEL_EDIT, true); - } - } catch (dpp::rest_exception &exception) { - set_test(VOICE_CHANNEL_EDIT, false); - } - - // delete the voice channel - try { - bot.channel_delete_sync(createdChannel.id); - set_test(VOICE_CHANNEL_DELETE, true); - } catch (dpp::rest_exception &exception) { - set_test(VOICE_CHANNEL_DELETE, false); - } - } - } - - set_test(FORUM_CREATION, false); - set_test(FORUM_CHANNEL_GET, false); - set_test(FORUM_CHANNEL_DELETE, false); - if (!offline) { - dpp::channel c; - c.name = "test-forum-channel"; - c.guild_id = TEST_GUILD_ID; - c.set_topic("This is a forum channel"); - c.set_flags(dpp::CHANNEL_FORUM); - c.default_sort_order = dpp::so_creation_date; - dpp::forum_tag t; - t.name = "Alpha"; - t.emoji = "❌"; - c.available_tags = {t}; - c.default_auto_archive_duration = dpp::arc_1_day; - c.default_reaction = "✅"; - c.default_thread_rate_limit_per_user = 10; - bot.channel_create(c, [&bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(FORUM_CREATION, true); - auto channel = std::get(event.value); - // retrieve the forum channel and check the values - bot.channel_get(channel.id, [forum_id = channel.id, &bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - auto channel = std::get(event.value); - bot.log(dpp::ll_debug, event.http_info.body); - bool tag = false; - for (auto &t : channel.available_tags) { - if (t.name == "Alpha" && std::holds_alternative(t.emoji) && std::get(t.emoji) == "❌") { - tag = true; - } - } - bool name = channel.name == "test-forum-channel"; - bool sort = channel.default_sort_order == dpp::so_creation_date; - bool rateLimit = channel.default_thread_rate_limit_per_user == 10; - set_test(FORUM_CHANNEL_GET, tag && name && sort && rateLimit); - } else { - set_test(FORUM_CHANNEL_GET, false); - } - // delete the forum channel - bot.channel_delete(forum_id, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(FORUM_CHANNEL_DELETE, true); - } else { - set_test(FORUM_CHANNEL_DELETE, false); - } - }); - }); - } else { - set_test(FORUM_CREATION, false); - set_test(FORUM_CHANNEL_GET, false); - } - }); - } - - set_test(THREAD_CREATE, false); - if (!offline) { - bot.thread_create("thread test", TEST_TEXT_CHANNEL_ID, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 60, [&](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - [[maybe_unused]] const auto &thread = event.get(); - set_test(THREAD_CREATE, true); - } - // the thread tests are in the on_thread_create event handler - }); - } - - set_test(MEMBER_GET, false); - if (!offline) { - bot.guild_get_member(TEST_GUILD_ID, TEST_USER_ID, [](const dpp::confirmation_callback_t &event){ - if (!event.is_error()) { - dpp::guild_member m = std::get(event.value); - if (m.guild_id == TEST_GUILD_ID && m.user_id == TEST_USER_ID) { - set_test(MEMBER_GET, true); - } else { - set_test(MEMBER_GET, false); - } - } else { - set_test(MEMBER_GET, false); - } - }); - } - - set_test(ROLE_CREATE, false); - set_test(ROLE_EDIT, false); - set_test(ROLE_DELETE, false); - if (!offline) { - dpp::role r; - r.guild_id = TEST_GUILD_ID; - r.name = "Test-Role"; - r.permissions.add(dpp::p_move_members); - r.set_flags(dpp::r_mentionable); - r.colour = dpp::colors::moon_yellow; - dpp::role createdRole; - try { - createdRole = bot.role_create_sync(r); - if (createdRole.name == r.name && - createdRole.has_move_members() && - createdRole.flags & dpp::r_mentionable && - createdRole.colour == r.colour) { - set_test(ROLE_CREATE, true); - } - } catch (dpp::rest_exception &exception) { - set_test(ROLE_CREATE, false); - } - createdRole.guild_id = TEST_GUILD_ID; - createdRole.name = "Test-Role-Edited"; - createdRole.colour = dpp::colors::light_sea_green; - try { - dpp::role edited = bot.role_edit_sync(createdRole); - if (createdRole.id == edited.id && edited.name == "Test-Role-Edited") { - set_test(ROLE_EDIT, true); - } - } catch (dpp::rest_exception &exception) { - set_test(ROLE_EDIT, false); - } - try { - bot.role_delete_sync(TEST_GUILD_ID, createdRole.id); - set_test(ROLE_DELETE, true); - } catch (dpp::rest_exception &exception) { - set_test(ROLE_DELETE, false); - } - } - }; - - set_test(BOTSTART, false); - try { - if (!offline) { - bot.start(true); - set_test(BOTSTART, true); - } - } - catch (const std::exception &) { - set_test(BOTSTART, false); - } - - set_test(TIMERSTART, false); - uint32_t ticks = 0; - dpp::timer th = bot.start_timer([&](dpp::timer timer_handle) { - if (ticks == 5) { - /* The simple test timer ticks every second. - * If we get to 5 seconds, we know the timer is working. - */ - set_test(TIMERSTART, true); - } - ticks++; - }, 1); - - set_test(USER_GET_CACHED_PRESENT, false); - try { - dpp::user_identified u = bot.user_get_cached_sync(TEST_USER_ID); - set_test(USER_GET_CACHED_PRESENT, (u.id == TEST_USER_ID)); - } - catch (const std::exception&) { - set_test(USER_GET_CACHED_PRESENT, false); - } - - set_test(USER_GET_CACHED_ABSENT, false); - try { - /* This is the snowflake ID of a discord staff member. - * We assume here that staffer's discord IDs will remain constant - * for long periods of time and they won't lurk in the unit test server. - * If this becomes not true any more, we'll pick another well known - * user ID. - */ - dpp::user_identified u = bot.user_get_cached_sync(90339695967350784); - set_test(USER_GET_CACHED_ABSENT, (u.id == dpp::snowflake(90339695967350784))); - } - catch (const std::exception&) { - set_test(USER_GET_CACHED_ABSENT, false); - } - - set_test(TIMEDLISTENER, false); - dpp::timed_listener tl(&bot, 10, bot.on_log, [&](const dpp::log_t & event) { - set_test(TIMEDLISTENER, true); - }); - - set_test(ONESHOT, false); - bool once = false; - dpp::oneshot_timer ost(&bot, 5, [&](dpp::timer timer_handle) { - if (!once) { - set_test(ONESHOT, true); - } else { - set_test(ONESHOT, false); - } - once = true; - }); - - set_test(CUSTOMCACHE, false); - dpp::cache testcache; - test_cached_object_t* tco = new test_cached_object_t(666); - tco->foo = "bar"; - testcache.store(tco); - test_cached_object_t* found_tco = testcache.find(666); - if (found_tco && found_tco->id == dpp::snowflake(666) && found_tco->foo == "bar") { - set_test(CUSTOMCACHE, true); - } else { - set_test(CUSTOMCACHE, false); - } - testcache.remove(found_tco); - - if (!offline) { - if (std::future_status status = ready_future.wait_for(std::chrono::seconds(20)); status != std::future_status::timeout) { - do_online_tests(); - } - } - - noparam_api_test(current_user_get, dpp::user_identified, CURRENTUSER); - singleparam_api_test(channel_get, TEST_TEXT_CHANNEL_ID, dpp::channel, GETCHAN); - singleparam_api_test(guild_get, TEST_GUILD_ID, dpp::guild, GETGUILD); - singleparam_api_test_list(roles_get, TEST_GUILD_ID, dpp::role_map, GETROLES); - singleparam_api_test_list(channels_get, TEST_GUILD_ID, dpp::channel_map, GETCHANS); - singleparam_api_test_list(guild_get_invites, TEST_GUILD_ID, dpp::invite_map, GETINVS); - multiparam_api_test_list(guild_get_bans, TEST_GUILD_ID, dpp::ban_map, GETBANS); - singleparam_api_test_list(channel_pins_get, TEST_TEXT_CHANNEL_ID, dpp::message_map, GETPINS); - singleparam_api_test_list(guild_events_get, TEST_GUILD_ID, dpp::scheduled_event_map, GETEVENTS); - twoparam_api_test(guild_event_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::scheduled_event, GETEVENT); - twoparam_api_test_list(guild_event_users_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::event_member_map, GETEVENTUSERS); - - std::this_thread::sleep_for(std::chrono::seconds(20)); - - /* Test stopping timer */ - set_test(TIMERSTOP, false); - set_test(TIMERSTOP, bot.stop_timer(th)); - - set_test(USERCACHE, false); - if (!offline) { - dpp::user* u = dpp::find_user(TEST_USER_ID); - set_test(USERCACHE, u); - } - set_test(CHANNELCACHE, false); - set_test(CHANNELTYPES, false); - if (!offline) { - dpp::channel* c = dpp::find_channel(TEST_TEXT_CHANNEL_ID); - dpp::channel* c2 = dpp::find_channel(TEST_VC_ID); - set_test(CHANNELCACHE, c && c2); - set_test(CHANNELTYPES, c && c->is_text_channel() && !c->is_voice_channel() && c2 && c2->is_voice_channel() && !c2->is_text_channel()); - } - - wait_for_tests(); - - } - catch (const std::exception &e) { - std::cout << e.what() << "\n"; - set_test(CLUSTER, false); - } - /* Return value = number of failed tests, exit code 0 = success */ return test_summary(); } diff --git a/src/unittest/test.h b/src/unittest/test.h index 7b2ee53e8b..343a0efb15 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -569,3 +569,33 @@ inline constexpr auto is_owner = [](auto &&user) noexcept { DPP_CHECK(test, std::is_copy_assignable_v, var); \ DPP_CHECK(test, std::is_move_assignable_v, var); \ } while(0) + +/** + * @brief Unit tests for Human readable error translation + */ +void errors_test(); + +/** +* @brief Unit tests for HTTPS client +*/ +void http_client_tests(const std::string&); + +/** +* @brief Unit tests for Discord objects (webhook, interaction, user etc.) +*/ +void discord_objects_tests(); + +/** + * @brief Unit tests for Gateway events + */ +void gateway_events_tests(const std::string&, dpp::cluster&); + +/** +* @brief Unit tests for Cache +*/ +void cache_tests(dpp::cluster&); + +/** +* @brief Unit tests for library Utilities +*/ +void utility_tests(); diff --git a/src/unittest/utilities.cpp b/src/unittest/utilities.cpp new file mode 100644 index 0000000000..5dbf37060d --- /dev/null +++ b/src/unittest/utilities.cpp @@ -0,0 +1,190 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include "test.h" + +#include + +/** + * @brief Type trait to check if a certain type has a build_json method + * + * @tparam T type to check for + */ +template > +struct has_build_json : std::false_type {}; + +template +struct has_build_json().build_json())>> : std::true_type {}; + +/** + * @brief Type trait to check if a certain type has a build_json method + * + * @tparam T type to check for + */ +template +constexpr bool has_build_json_v = has_build_json::value; + +/** + * @brief Type trait to check if a certain type has a fill_from_json method + * + * @tparam T type to check for + */ +template +struct has_fill_from_json : std::false_type {}; + +template +struct has_fill_from_json().fill_from_json(std::declval()))>> : std::true_type {}; + +/** + * @brief Type trait to check if a certain type has a fill_from_json method + * + * @tparam T type to check for + */ +template +constexpr bool has_fill_from_json_v = has_fill_from_json::value; + +/* Unit tests for library utilities */ +void utility_tests() { + // markdown escape tests + std::string test_to_escape = "*** _This is a test_ ***\n```cpp\n\ +int main() {\n\ + /* Comment */\n\ + int answer = 42;\n\ + return answer; // ___\n\ +};\n\ +```\n\ +Markdown lol ||spoiler|| ~~strikethrough~~ `small *code* block`\n"; + + set_test(MD_ESC_1, false); + set_test(MD_ESC_2, false); + std::string escaped1 = dpp::utility::markdown_escape(test_to_escape); + std::string escaped2 = dpp::utility::markdown_escape(test_to_escape, true); + set_test(MD_ESC_1, escaped1 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ +```cpp\n\ +int main() {\n\ + /* Comment */\n\ + int answer = 42;\n\ + return answer; // ___\n\ +};\n\ +```\n\ +Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ `small *code* block`\n"); + set_test(MD_ESC_2, escaped2 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ +\\`\\`\\`cpp\n\ +int main\\(\\) {\n\ + /\\* Comment \\*/\n\ + int answer = 42;\n\ + return answer; // \\_\\_\\_\n\ +};\n\ +\\`\\`\\`\n\ +Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* block\\`\n"); + + + set_test(COMPARISON, false); + dpp::user u1; + dpp::user u2; + dpp::user u3; + u1.id = u2.id = 666; + u3.id = 777; + set_test(COMPARISON, u1 == u2 && u1 != u3); + + // encoding tests + set_test(URLENC, false); + set_test(URLENC, dpp::utility::url_encode("ABC123_+\\|$*/AAA[]😄") == "ABC123_%2B%5C%7C%24%2A%2FAAA%5B%5D%F0%9F%98%84"); + + set_test(BASE64ENC, false); + set_test(BASE64ENC, + dpp::base64_encode(reinterpret_cast("a"), 1) == "YQ==" && + dpp::base64_encode(reinterpret_cast("bc"), 2) == "YmM=" && + dpp::base64_encode(reinterpret_cast("def"), 3) == "ZGVm" && + dpp::base64_encode(reinterpret_cast("ghij"), 4) == "Z2hpag==" && + dpp::base64_encode(reinterpret_cast("klmno"), 5) == "a2xtbm8=" && + dpp::base64_encode(reinterpret_cast("pqrstu"), 6) == "cHFyc3R1" && + dpp::base64_encode(reinterpret_cast("vwxyz12"), 7) == "dnd4eXoxMg==" + ); + + set_test(READFILE, false); + std::string rf_test = dpp::utility::read_file(SHARED_OBJECT); + FILE* fp = fopen(SHARED_OBJECT, "rb"); + fseek(fp, 0, SEEK_END); + size_t off = (size_t)ftell(fp); + fclose(fp); + set_test(READFILE, off == rf_test.length()); + + set_test(TIMESTAMPTOSTRING, false); + set_test(TIMESTAMPTOSTRING, dpp::ts_to_string(1642611864) == "2022-01-19T17:04:24Z"); + +#ifndef _WIN32 + set_test(TIMESTRINGTOTIMESTAMP, false); + json tj; + tj["t1"] = "2022-01-19T17:18:14.506000+00:00"; + tj["t2"] = "2022-01-19T17:18:14+00:00"; + uint32_t inTimestamp = 1642612694; + set_test(TIMESTRINGTOTIMESTAMP, (uint64_t)dpp::ts_not_null(&tj, "t1") == inTimestamp && (uint64_t)dpp::ts_not_null(&tj, "t2") == inTimestamp); +#else + set_test(TIMESTRINGTOTIMESTAMP, true); +#endif + + { + set_test(TS, false); + dpp::managed m(189759562910400512); + set_test(TS, ((uint64_t) m.get_creation_time()) == 1465312605); + } + + { // test dpp::json_interface + start_test(JSON_INTERFACE); + struct fillable : dpp::json_interface { + fillable &fill_from_json_impl(dpp::json *) { + return *this; + } + }; + struct buildable : dpp::json_interface { + json to_json_impl(bool = false) const { + return {}; + } + }; + struct fillable_and_buildable : dpp::json_interface { + fillable_and_buildable &fill_from_json_impl(dpp::json *) { + return *this; + } + + json to_json_impl(bool = false) const { + return {}; + } + }; + bool success = true; + + DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); + DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); + DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v, success); + + DPP_CHECK(JSON_INTERFACE, !has_build_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); + DPP_CHECK(JSON_INTERFACE, !has_build_json_v, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); + + DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); + set_test(JSON_INTERFACE, success); + } +} From af1af4266c62378dc4bd590d35bc8633b64d432b Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Sat, 21 Oct 2023 16:08:01 +0100 Subject: [PATCH 04/14] docs: comments are now more specific in code style. (#964) --- .../coding_style_standards.md | 75 +++---------------- .../coding_style_standards/comments.cpp | 27 +++++++ .../coding_style_standards/curly_braces.cpp | 20 +++++ .../coding_style_standards/dot_notation.cpp | 5 ++ .../coding_style_standards/fluent_design.cpp | 18 +++++ .../coding_style_standards/fluent_design2.cpp | 2 + .../coding_style_standards/lists.cpp | 3 + .../symbol_exporting.cpp | 5 ++ 8 files changed, 91 insertions(+), 64 deletions(-) create mode 100644 docpages/example_code/coding_style_standards/comments.cpp create mode 100644 docpages/example_code/coding_style_standards/curly_braces.cpp create mode 100644 docpages/example_code/coding_style_standards/dot_notation.cpp create mode 100644 docpages/example_code/coding_style_standards/fluent_design.cpp create mode 100644 docpages/example_code/coding_style_standards/fluent_design2.cpp create mode 100644 docpages/example_code/coding_style_standards/lists.cpp create mode 100644 docpages/example_code/coding_style_standards/symbol_exporting.cpp diff --git a/docpages/advanced_reference/coding_style_standards.md b/docpages/advanced_reference/coding_style_standards.md index 4fb733cc17..6885a1d1f4 100644 --- a/docpages/advanced_reference/coding_style_standards.md +++ b/docpages/advanced_reference/coding_style_standards.md @@ -18,28 +18,7 @@ This covers your standard Curly Braces (commonly known as squiggly brackets), an Curly Braces should be on the same line as the keyword, for example: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp -void foo() { - if (a == b) { - c(); - } else { - d(); - } - - while (true) { - // ... - } - - switch (a) { - case 1: - c(); - break; - case 2: - d(); - break; - } -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} coding_style_standards/curly_braces.cpp This applies to functions, `while` statements, `if` statements, lambdas, nearly anything that uses curly braces with statements! @@ -47,20 +26,12 @@ This applies to functions, `while` statements, `if` statements, lambdas, nearly Lists should have a space after the comma in parameter lists, and after opening brackets and before closing brackets except when calling a function, for example: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp -std::vector clowns = { "pennywise", "bobo" }; - -evaluate_clown(clowns[0], evilness(2.5, factor)); -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} coding_style_standards/lists.cpp ## Dot (.) Notation When using the dot notation repeatedly (For example, creating an embed.), you should start each `.function()` on a new line, as such: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp -stuff{} - .add_stuff() - .add_stuff(); -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} coding_style_standards/dot_notation.cpp ## Indentation @@ -72,7 +43,11 @@ Constants and macros should be all `UPPERCASE` with `SNAKE_CASE` to separate wor ## Comments -All comments should be in `doxygen` format (similar to javadoc). Please see existing class definitions for an example. You should use doxygen style comments in a class definition inside a header file, and can use any other comment types within the .cpp file. Be liberal with comments, especially if your code makes any assumptions! +All comments should be in `doxygen` format (similar to javadoc). Please see existing class definitions for an example. You should use doxygen style comments in a class definition inside a header file. Be liberal with comments, especially if your code makes any assumptions! Comments should follow the format below: + +\note Comments that contain doxygen stuff need to use two stars at the beginning (/**). This example doesn't because doxygen gets confused and doesn't show the comments. + +\include{cpp} coding_style_standards/comments.cpp ## Spell Checks @@ -82,13 +57,7 @@ To prevent typos, a GitHub-Action checks the documentation. If it fails for a wo If you export a class which is to be accessible to users, be sure to prefix it with the `DPP_EXPORT` macro, for example: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp -class DPP_EXPORT my_new_class { -public: - int hats; - int clowns; -}; -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} coding_style_standards/symbol_exporting.cpp The `DPP_EXPORT` macro ensures that on certain platforms (notably Windows) the symbol is exported to be available to the library user. @@ -132,33 +101,11 @@ If a value will only hold values up to 255, use `uint8_t`. If a value cannot hol Where possible, if you are adding methods to a class you should consider fluent design. Fluent design is the use of class methods that return a reference to self (via `return *this`), so that you can chain object method calls together (in the way dpp::message and dpp::embed do). For example: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp -class DPP_EXPORT my_new_class { -public: - int hats; - int clowns; - - my_new_class& set_hats(int new_hats); - my_new_class& set_clowns(int new_clowns); -}; - -my_new_class& my_new_class::set_hats(int new_hats) { - hats = new_hats; - return *this; -} - -my_new_class& my_new_class::set_clowns(int new_clowns) { - clowns = new_clowns; - return *this; -} -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} coding_style_standards/fluent_design.cpp This would allow the user to do this: -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~cpp -dpp::my_new_class nc; -nc.set_hats(3).set_clowns(9001); -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +\include{cpp} coding_style_standards/fluent_design2.cpp ## Keep All D++ Related Types in the dpp Namespace diff --git a/docpages/example_code/coding_style_standards/comments.cpp b/docpages/example_code/coding_style_standards/comments.cpp new file mode 100644 index 0000000000..18a41158fb --- /dev/null +++ b/docpages/example_code/coding_style_standards/comments.cpp @@ -0,0 +1,27 @@ +/* + * @brief This is a function that does some cool stuff. + * More stuff here will still go in brief! + * @warning This does nothing! + */ +func_name(); + +/* + * @brief This turns a name into a meme name! + * + * @param name The name of the user that you want to meme-ify. + * @return a meme name! + */ +std::string name_to_meme(const std::string& name) const; + +/* -------------------- .cpp file -------------------- */ + +int main() { + /* We are now going to do some cool stuff. */ + func_name(); + + /* Going to turn brain into a meme name. + * Why? + * Because why not. That's why. + */ + std::cout << name_to_meme("Brain") << "\n"; +} diff --git a/docpages/example_code/coding_style_standards/curly_braces.cpp b/docpages/example_code/coding_style_standards/curly_braces.cpp new file mode 100644 index 0000000000..02662f6436 --- /dev/null +++ b/docpages/example_code/coding_style_standards/curly_braces.cpp @@ -0,0 +1,20 @@ +void foo() { + if (a == b) { + c(); + } else { + d(); + } + + while (true) { + // ... + } + + switch (a) { + case 1: + c(); + break; + case 2: + d(); + break; + } +} diff --git a/docpages/example_code/coding_style_standards/dot_notation.cpp b/docpages/example_code/coding_style_standards/dot_notation.cpp new file mode 100644 index 0000000000..2c0a410bfb --- /dev/null +++ b/docpages/example_code/coding_style_standards/dot_notation.cpp @@ -0,0 +1,5 @@ +stuff{} + .add_stuff() + .add_stuff(); + +event.reply("This reply function isn't indented!"); diff --git a/docpages/example_code/coding_style_standards/fluent_design.cpp b/docpages/example_code/coding_style_standards/fluent_design.cpp new file mode 100644 index 0000000000..8a9d55c101 --- /dev/null +++ b/docpages/example_code/coding_style_standards/fluent_design.cpp @@ -0,0 +1,18 @@ +class DPP_EXPORT my_new_class { +public: + int hats; + int clowns; + + my_new_class& set_hats(int new_hats); + my_new_class& set_clowns(int new_clowns); +}; + +my_new_class& my_new_class::set_hats(int new_hats) { + hats = new_hats; + return *this; +} + +my_new_class& my_new_class::set_clowns(int new_clowns) { + clowns = new_clowns; + return *this; +} diff --git a/docpages/example_code/coding_style_standards/fluent_design2.cpp b/docpages/example_code/coding_style_standards/fluent_design2.cpp new file mode 100644 index 0000000000..fa2416f438 --- /dev/null +++ b/docpages/example_code/coding_style_standards/fluent_design2.cpp @@ -0,0 +1,2 @@ +dpp::my_new_class nc; +nc.set_hats(3).set_clowns(9001); diff --git a/docpages/example_code/coding_style_standards/lists.cpp b/docpages/example_code/coding_style_standards/lists.cpp new file mode 100644 index 0000000000..b67a80d5ff --- /dev/null +++ b/docpages/example_code/coding_style_standards/lists.cpp @@ -0,0 +1,3 @@ +std::vector clowns = { "pennywise", "bobo" }; + +evaluate_clown(clowns[0], evilness(2.5, factor)); diff --git a/docpages/example_code/coding_style_standards/symbol_exporting.cpp b/docpages/example_code/coding_style_standards/symbol_exporting.cpp new file mode 100644 index 0000000000..f37e9cf7f0 --- /dev/null +++ b/docpages/example_code/coding_style_standards/symbol_exporting.cpp @@ -0,0 +1,5 @@ +class DPP_EXPORT my_new_class { +public: + int hats; + int clowns; +}; From cad6668e8c1dc15e78a5c9463671f25dbab95cff Mon Sep 17 00:00:00 2001 From: Eric <52634785+erics118@users.noreply.github.com> Date: Sat, 21 Oct 2023 11:08:32 -0400 Subject: [PATCH 05/14] feat: support subcommand autocomplete (#963) --- src/dpp/events/interaction_create.cpp | 75 ++++++++++++++------------- 1 file changed, 40 insertions(+), 35 deletions(-) diff --git a/src/dpp/events/interaction_create.cpp b/src/dpp/events/interaction_create.cpp index e7a0297473..f244f65d0a 100644 --- a/src/dpp/events/interaction_create.cpp +++ b/src/dpp/events/interaction_create.cpp @@ -28,6 +28,44 @@ namespace dpp::events { +namespace { + +void fill_options(dpp::json option_json, std::vector& options) { + for (auto & o : option_json) { + dpp::command_option opt; + opt.name = string_not_null(&o, "name"); + opt.type = (dpp::command_option_type)int8_not_null(&o, "type"); + switch (opt.type) { + case co_boolean: + opt.value = o.at("value").get(); + break; + case co_channel: + case co_role: + case co_user: + case co_attachment: + case co_mentionable: + opt.value = dpp::snowflake(snowflake_not_null(&o, "value")); + break; + case co_integer: + opt.value = o.at("value").get(); + break; + case co_string: + opt.value = o.at("value").get(); + break; + case co_number: + opt.value = o.at("value").get(); + break; + case co_sub_command: + case co_sub_command_group: + fill_options(o["options"], opt.options); + break; + } + opt.focused = bool_not_null(&o, "focused"); + options.emplace_back(opt); + } +} + +} /** * @brief Handle event @@ -96,40 +134,7 @@ void interaction_create::handle(discord_client* client, json &j, const std::stri dpp::autocomplete_t ac(client, raw); ac.id = snowflake_not_null(&(d["data"]), "id"); ac.name = string_not_null(&(d["data"]), "name"); - for (auto & o : d["data"]["options"]) { - dpp::command_option opt; - opt.name = string_not_null(&o, "name"); - opt.type = (dpp::command_option_type)int8_not_null(&o, "type"); - if (o.contains("value") && !o.at("value").is_null()) { - switch (opt.type) { - case co_boolean: - opt.value = o.at("value").get(); - break; - case co_channel: - case co_role: - case co_user: - case co_attachment: - case co_mentionable: - opt.value = dpp::snowflake(snowflake_not_null(&o, "value")); - break; - case co_integer: - opt.value = o.at("value").get(); - break; - case co_string: - opt.value = o.at("value").get(); - break; - case co_number: - opt.value = o.at("value").get(); - break; - case co_sub_command: - case co_sub_command_group: - /* Silences warning on clang, handled elsewhere */ - break; - } - } - opt.focused = bool_not_null(&o, "focused"); - ac.options.emplace_back(opt); - } + fill_options(d["data"]["options"], ac.options); ac.command = i; client->creator->on_autocomplete.call(ac); } @@ -158,4 +163,4 @@ void interaction_create::handle(discord_client* client, json &j, const std::stri } } -}; \ No newline at end of file +}; From f19db87d70343398c5b28d33e2b837d1ac2799f8 Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Sat, 21 Oct 2023 16:09:18 +0100 Subject: [PATCH 06/14] feat: added roles to emojis (#959) --- include/dpp/emoji.h | 22 +++++++++++++++------- src/dpp/emoji.cpp | 22 ++++++++++++++++------ 2 files changed, 31 insertions(+), 13 deletions(-) diff --git a/include/dpp/emoji.h b/include/dpp/emoji.h index 69f8a7b3b5..b8eba59cf5 100644 --- a/include/dpp/emoji.h +++ b/include/dpp/emoji.h @@ -72,21 +72,29 @@ class DPP_EXPORT emoji : public managed, public json_interface { public: /** - * @brief Emoji name + * @brief Emoji name. */ std::string name{}; + /** - * @brief User id who uploaded the emoji + * @brief Roles allowed to use this emoji. */ - snowflake user_id{0}; + std::vector roles; + /** - * @brief Flags for the emoji from dpp::emoji_flags + * @brief The id of the user that created this emoji. */ - uint8_t flags{0}; + snowflake user_id; + /** - * @brief Image data for the emoji if uploading + * @brief Image data for the emoji, if uploading. */ - std::string image_data{}; + std::string image_data; + + /** + * @brief Flags for the emoji from dpp::emoji_flags. + */ + uint8_t flags{0}; /** * @brief Construct a new emoji object diff --git a/src/dpp/emoji.cpp b/src/dpp/emoji.cpp index b033366e32..7793c34c85 100644 --- a/src/dpp/emoji.cpp +++ b/src/dpp/emoji.cpp @@ -41,6 +41,13 @@ emoji& emoji::fill_from_json_impl(nlohmann::json* j) { json & user = (*j)["user"]; user_id = snowflake_not_null(&user, "id"); } + + if(j->contains("roles")) { + for (const auto& role : (*j)["roles"]) { + this->roles.emplace_back(to_string(role)); + } + } + if (bool_not_null(j, "require_colons")) { flags |= e_require_colons; } @@ -65,6 +72,10 @@ json emoji::to_json_impl(bool with_id) const { if (!image_data.empty()) { j["image"] = image_data; } + j["roles"] = json::array(); + for (const auto& role : roles) { + j["roles"].push_back(role.str()); + } return j; } @@ -94,8 +105,7 @@ emoji& emoji::load_image(std::string_view image_blob, const image_type type) { return *this; } -std::string emoji::format() const -{ +std::string emoji::format() const { return id ? ((is_animated() ? "a:" : "") + name + ":" + std::to_string(id)) : name; } @@ -106,11 +116,11 @@ std::string emoji::get_mention() const { std::string emoji::get_url(uint16_t size, const dpp::image_type format, bool prefer_animated) const { if (this->id) { return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp, i_gif }, - "emojis/" + std::to_string(this->id), - format, size, prefer_animated, is_animated()); - } else { - return std::string(); + "emojis/" + std::to_string(this->id), + format, size, prefer_animated, is_animated()); } + + return ""; } From 82f9297965a2a1cc0789f3d2c8e48c33c283d0ba Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Sat, 21 Oct 2023 18:05:51 +0100 Subject: [PATCH 07/14] feat: added application and following support for webhooks. (#960) --- include/dpp/webhook.h | 90 ++++++++++++++++++++++++++++++------- src/dpp/cluster/webhook.cpp | 21 +++------ src/dpp/webhook.cpp | 59 +++++++++--------------- 3 files changed, 100 insertions(+), 70 deletions(-) diff --git a/include/dpp/webhook.h b/include/dpp/webhook.h index 808eb45227..0593a9fcb4 100644 --- a/include/dpp/webhook.h +++ b/include/dpp/webhook.h @@ -25,6 +25,9 @@ #include #include #include +#include +#include +#include #include #include @@ -35,7 +38,8 @@ namespace dpp { */ enum webhook_type { w_incoming = 1, //!< Incoming webhook - w_channel_follower = 2 //!< Channel following webhook + w_channel_follower = 2, //!< Channel following webhook + w_application = 3 //!< Application webhooks for interactions. }; /** @@ -62,15 +66,76 @@ class DPP_EXPORT webhook : public managed, public json_interface { virtual json to_json_impl(bool with_id = false) const; public: - uint8_t type; //!< the type of the webhook - snowflake guild_id; //!< Optional: the guild id this webhook is for - snowflake channel_id; //!< the channel id this webhook is for - snowflake user_id; //!< Optional: the user this webhook was created by (not returned when getting a webhook with its token) - std::string name; //!< the default name of the webhook (may be empty) - std::string avatar; //!< the default avatar of the webhook (may be empty) - std::string token; //!< Optional: the secure token of the webhook (returned for Incoming Webhooks) - snowflake application_id; //!< the bot/OAuth2 application that created this webhook (may be empty) - std::string* image_data; //!< base64 encoded image data if uploading a new image + /** + * @brief Type of the webhook from dpp::webhook_type. + */ + uint8_t type; + + /** + * @brief The guild id this webhook is for. + * @note This field is optional, and may also be empty. + */ + snowflake guild_id; + + /** + * @brief The channel id this webhook is for. + * @note This may be empty. + */ + snowflake channel_id; + + /** + * @brief The user this webhook was created by. + * @note This field is optional. + * @warning This is not returned when getting a webhook with its token! + */ + user user_obj; + + /** + * @brief The default name of the webhook. + * @note This may be empty. + */ + std::string name; + + /** + * @brief The default avatar of the webhook + * @note This may be empty. + */ + utility::iconhash avatar; + + /** + * @brief The secure token of the webhook (returned for Incoming Webhooks). + * @note This field is optional. + */ + std::string token; + + /** + * @brief The bot/OAuth2 application that created this webhook. + * @note This may be empty. + */ + snowflake application_id; + + /** + * @brief The guild of the channel that this webhook is following (only for Channel Follower Webhooks). + * @warning This will be absent if the webhook creator has since lost access to the guild where the followed channel resides! + */ + guild source_guild; + + /** + * @brief The channel that this webhook is following (only for Channel Follower Webhooks). + * @warning This will be absent if the webhook creator has since lost access to the guild where the followed channel resides! + */ + channel source_channel; + + /** + * @brief The url used for executing the webhook (returned by the webhooks OAuth2 flow). + */ + std::string url; + + /** + * @brief base64 encoded image data if uploading a new image. + * @warning You should only ever read data from here. If you want to set the data, use dpp::webhook::load_image. + */ + std::string image_data; /** * @brief Construct a new webhook object @@ -93,11 +158,6 @@ class DPP_EXPORT webhook : public managed, public json_interface { */ webhook(const snowflake webhook_id, const std::string& webhook_token); - /** - * @brief Destroy the webhook object - */ - ~webhook(); - /** * @brief Base64 encode image data and allocate it to image_data * diff --git a/src/dpp/cluster/webhook.cpp b/src/dpp/cluster/webhook.cpp index f182012ec2..609db9ae1b 100644 --- a/src/dpp/cluster/webhook.cpp +++ b/src/dpp/cluster/webhook.cpp @@ -23,16 +23,14 @@ namespace dpp { -void cluster::create_webhook(const class webhook &w, command_completion_event_t callback) { - rest_request(this, API_PATH "/channels", std::to_string(w.channel_id), "webhooks", m_post, w.build_json(false), callback); +void cluster::create_webhook(const class webhook &wh, command_completion_event_t callback) { + rest_request(this, API_PATH "/channels", std::to_string(wh.channel_id), "webhooks", m_post, wh.build_json(false), callback); } - void cluster::delete_webhook(snowflake webhook_id, command_completion_event_t callback) { rest_request(this, API_PATH "/webhooks", std::to_string(webhook_id), "", m_delete, "", callback); } - void cluster::delete_webhook_message(const class webhook &wh, snowflake message_id, snowflake thread_id, command_completion_event_t callback) { std::string parameters = utility::make_url_parameters({ {"thread_id", thread_id}, @@ -40,17 +38,14 @@ void cluster::delete_webhook_message(const class webhook &wh, snowflake message_ rest_request(this, API_PATH "/webhooks", std::to_string(wh.id), utility::url_encode(!wh.token.empty() ? wh.token: token) + "/messages/" + std::to_string(message_id) + parameters, m_delete, "", callback); } - void cluster::delete_webhook_with_token(snowflake webhook_id, const std::string &token, command_completion_event_t callback) { rest_request(this, API_PATH "/webhooks", std::to_string(webhook_id), utility::url_encode(token), m_delete, "", callback); } - void cluster::edit_webhook(const class webhook& wh, command_completion_event_t callback) { rest_request(this, API_PATH "/webhooks", std::to_string(wh.id), "", m_patch, wh.build_json(false), callback); } - void cluster::edit_webhook_message(const class webhook &wh, const struct message& m, snowflake thread_id, command_completion_event_t callback) { std::string parameters = utility::make_url_parameters({ {"thread_id", thread_id}, @@ -62,7 +57,6 @@ void cluster::edit_webhook_message(const class webhook &wh, const struct message }, m.filename, m.filecontent, m.filemimetype); } - void cluster::edit_webhook_with_token(const class webhook& wh, command_completion_event_t callback) { json jwh = wh.to_json(true); if (jwh.find("channel_id") != jwh.end()) { @@ -71,20 +65,19 @@ void cluster::edit_webhook_with_token(const class webhook& wh, command_completio rest_request(this, API_PATH "/webhooks", std::to_string(wh.id), utility::url_encode(wh.token), m_patch, jwh.dump(), callback); } - void cluster::execute_webhook(const class webhook &wh, const struct message& m, bool wait, snowflake thread_id, const std::string& thread_name, command_completion_event_t callback) { std::string parameters = utility::make_url_parameters({ {"wait", wait}, {"thread_id", thread_id}, }); std::string body; - if (!thread_name.empty() || !wh.avatar.empty() || !wh.name.empty()) { // only use json::parse if thread_name is set + if (!thread_name.empty() || !wh.avatar.to_string().empty() || !wh.name.empty()) { // only use json::parse if thread_name is set json j = m.to_json(false); if (!thread_name.empty()) { j["thread_name"] = thread_name; } - if (!wh.avatar.empty()) { - j["avatar_url"] = wh.avatar; + if (!wh.avatar.to_string().empty()) { + j["avatar_url"] = wh.avatar.to_string(); } if (!wh.name.empty()) { j["username"] = wh.name; @@ -98,7 +91,6 @@ void cluster::execute_webhook(const class webhook &wh, const struct message& m, }, m.filename, m.filecontent, m.filemimetype); } - void cluster::get_channel_webhooks(snowflake channel_id, command_completion_event_t callback) { rest_request_list(this, API_PATH "/channels", std::to_string(channel_id), "webhooks", m_get, "", callback); } @@ -108,12 +100,10 @@ void cluster::get_guild_webhooks(snowflake guild_id, command_completion_event_t rest_request_list(this, API_PATH "/guilds", std::to_string(guild_id), "webhooks", m_get, "", callback); } - void cluster::get_webhook(snowflake webhook_id, command_completion_event_t callback) { rest_request(this, API_PATH "/webhooks", std::to_string(webhook_id), "", m_get, "", callback); } - void cluster::get_webhook_message(const class webhook &wh, snowflake message_id, snowflake thread_id, command_completion_event_t callback) { std::string parameters = utility::make_url_parameters({ {"thread_id", thread_id}, @@ -121,7 +111,6 @@ void cluster::get_webhook_message(const class webhook &wh, snowflake message_id, rest_request(this, API_PATH "/webhooks", std::to_string(wh.id), utility::url_encode(!wh.token.empty() ? wh.token: token) + "/messages/" + std::to_string(message_id) + parameters, m_get, "", callback); } - void cluster::get_webhook_with_token(snowflake webhook_id, const std::string &token, command_completion_event_t callback) { rest_request(this, API_PATH "/webhooks", std::to_string(webhook_id), utility::url_encode(token), m_get, "", callback); } diff --git a/src/dpp/webhook.cpp b/src/dpp/webhook.cpp index 9ef46d2df6..4b3c23a620 100644 --- a/src/dpp/webhook.cpp +++ b/src/dpp/webhook.cpp @@ -30,7 +30,7 @@ using json = nlohmann::json; const size_t MAX_ICON_SIZE = 256 * 1024; -webhook::webhook() : managed(), type(w_incoming), guild_id(0), channel_id(0), user_id(0), application_id(0), image_data(nullptr) +webhook::webhook() : managed(), type(w_incoming), guild_id(0), channel_id(0), application_id(0) { } @@ -56,66 +56,47 @@ webhook::webhook(const snowflake webhook_id, const std::string& webhook_token) : id = webhook_id; } -webhook::~webhook() { - delete image_data; -} - webhook& webhook::fill_from_json_impl(nlohmann::json* j) { - id = snowflake_not_null(j, "id"); - type = int8_not_null(j, "type"); - channel_id = snowflake_not_null(j, "channel_id"); - guild_id = snowflake_not_null(j, "guild_id"); + set_snowflake_not_null(j, "id", id); + set_int8_not_null(j, "type", type); + set_snowflake_not_null(j, "guild_id", guild_id); + set_snowflake_not_null(j, "channel_id", channel_id); if (j->contains("user")) { - json & user = (*j)["user"]; - user_id = snowflake_not_null(&user, "id"); + user_obj = user().fill_from_json(&((*j)["user"])); + } + set_string_not_null(j, "name", name); + set_iconhash_not_null(j, "avatar", avatar); + set_string_not_null(j, "token", token); + set_snowflake_not_null(j, "application_id", application_id); + if (j->contains("source_guild")) { + source_guild = guild().fill_from_json(&((*j)["source_guild"])); + } + if (j->contains("source_channel")) { + source_channel = channel().fill_from_json(&((*j)["source_channel"])); } - name = string_not_null(j, "name"); - avatar = string_not_null(j, "avatar"); - token = string_not_null(j, "token"); - application_id = snowflake_not_null(j, "application_id"); + set_string_not_null(j, "url", url); return *this; } json webhook::to_json_impl(bool with_id) const { json j; - if (with_id) { - j["id"] = std::to_string(id); - } j["name"] = name; - j["type"] = type; if (channel_id) { j["channel_id"] = channel_id; } - if (guild_id) { - j["guild_id"] = guild_id; - } - if (!name.empty()) { - j["name"] = name; - } - if (image_data) { - j["avatar"] = *image_data; - } - if (application_id) { - j["application_id"] = application_id; + if (!image_data.empty()) { + j["avatar"] = image_data; } return j; } webhook& webhook::load_image(const std::string &image_blob, const image_type type, bool is_base64_encoded) { - static const std::map mimetypes = { - { i_gif, "image/gif" }, - { i_jpg, "image/jpeg" }, - { i_png, "image/png" }, - { i_webp, "image/webp" }, - }; if (image_blob.size() > MAX_ICON_SIZE) { throw dpp::length_exception("Webhook icon file exceeds discord limit of 256 kilobytes"); } - /* If there's already image data defined, free the old data, to prevent a memory leak */ - delete image_data; - image_data = new std::string("data:" + mimetypes.find(type)->second + ";base64," + (is_base64_encoded ? image_blob : base64_encode((unsigned char const*)image_blob.data(), (unsigned int)image_blob.length()))); + image_data = "data:" + utility::mime_type(type) + ";base64," + (is_base64_encoded ? image_blob : base64_encode(reinterpret_cast(image_blob.data()), static_cast(image_blob.length()))); return *this; } From 8d379b6def79c97da1d4f7fadf5f53771cd2129c Mon Sep 17 00:00:00 2001 From: "Craig Edwards (Brain)" Date: Sun, 22 Oct 2023 14:28:18 +0100 Subject: [PATCH 08/14] Revert "refactor: cleaned up Unit Tests" (#966) --- src/unittest/cache.cpp | 64 - src/unittest/discord_objects.cpp | 567 -------- src/unittest/errors.cpp | 118 -- src/unittest/gateway_events.cpp | 1296 ------------------ src/unittest/http.cpp | 93 -- src/unittest/test.cpp | 2199 +++++++++++++++++++++++++++++- src/unittest/test.h | 30 - src/unittest/utilities.cpp | 190 --- 8 files changed, 2192 insertions(+), 2365 deletions(-) delete mode 100644 src/unittest/cache.cpp delete mode 100644 src/unittest/discord_objects.cpp delete mode 100644 src/unittest/errors.cpp delete mode 100644 src/unittest/gateway_events.cpp delete mode 100644 src/unittest/http.cpp delete mode 100644 src/unittest/utilities.cpp diff --git a/src/unittest/cache.cpp b/src/unittest/cache.cpp deleted file mode 100644 index 6892e090cb..0000000000 --- a/src/unittest/cache.cpp +++ /dev/null @@ -1,64 +0,0 @@ -/************************************************************************************ - * - * D++, A Lightweight C++ library for Discord - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2021 Craig Edwards and D++ contributors - * (https://github.com/brainboxdotcc/DPP/graphs/contributors) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ************************************************************************************/ -#include "test.h" - -#include - -/* Unit tests for Cache */ -void cache_tests(dpp::cluster& bot) { - set_test(USER_GET_CACHED_PRESENT, false); - try { - dpp::user_identified u = bot.user_get_cached_sync(TEST_USER_ID); - set_test(USER_GET_CACHED_PRESENT, (u.id == TEST_USER_ID)); - } - catch (const std::exception&) { - set_test(USER_GET_CACHED_PRESENT, false); - } - - set_test(USER_GET_CACHED_ABSENT, false); - try { - /* This is the snowflake ID of a discord staff member. - * We assume here that staffer's discord IDs will remain constant - * for long periods of time and they won't lurk in the unit test server. - * If this becomes not true any more, we'll pick another well known - * user ID. - */ - dpp::user_identified u = bot.user_get_cached_sync(90339695967350784); - set_test(USER_GET_CACHED_ABSENT, (u.id == dpp::snowflake(90339695967350784))); - } - catch (const std::exception&) { - set_test(USER_GET_CACHED_ABSENT, false); - } - - set_test(CUSTOMCACHE, false); - dpp::cache testcache; - test_cached_object_t* tco = new test_cached_object_t(666); - tco->foo = "bar"; - testcache.store(tco); - test_cached_object_t* found_tco = testcache.find(666); - if (found_tco && found_tco->id == dpp::snowflake(666) && found_tco->foo == "bar") { - set_test(CUSTOMCACHE, true); - } else { - set_test(CUSTOMCACHE, false); - } - testcache.remove(found_tco); -} diff --git a/src/unittest/discord_objects.cpp b/src/unittest/discord_objects.cpp deleted file mode 100644 index 0bd1ff9eae..0000000000 --- a/src/unittest/discord_objects.cpp +++ /dev/null @@ -1,567 +0,0 @@ -/************************************************************************************ - * - * D++, A Lightweight C++ library for Discord - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2021 Craig Edwards and D++ contributors - * (https://github.com/brainboxdotcc/DPP/graphs/contributors) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ************************************************************************************/ -#include "test.h" - -/* Unit tests for Discord objects (webhook, interaction, user etc.) */ -void discord_objects_tests() { - // test webhook - set_test(WEBHOOK, false); - try { - dpp::webhook test_wh("https://discord.com/api/webhooks/833047646548133537/ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE"); - set_test(WEBHOOK, (test_wh.token == "ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE") && (test_wh.id == dpp::snowflake(833047646548133537))); - } - catch (const dpp::exception&) { - set_test(WEBHOOK, false); - } - - { // test dpp::snowflake - start_test(SNOWFLAKE); - bool success = true; - dpp::snowflake s = 69420; - json j; - j["value"] = s; - success = dpp::snowflake_not_null(&j, "value") == 69420 && success; - DPP_CHECK_CONSTRUCT_ASSIGN(SNOWFLAKE, dpp::snowflake, success); - s = 42069; - success = success && (s == 42069 && s == dpp::snowflake{42069} && s == "42069"); - success = success && (dpp::snowflake{69} < dpp::snowflake{420} && (dpp::snowflake{69} < 420)); - s = "69420"; - success = success && s == 69420; - auto conversion_test = [](dpp::snowflake sl) { - return sl.str(); - }; - s = conversion_test(std::string{"1337"}); - success = success && s == 1337; /* THIS BREAKS (and i do not care very much): && s == conversion_test(dpp::snowflake{"1337"}); */ - success = success && dpp::snowflake{0} == 0; - set_test(SNOWFLAKE, success); - } - - { // test interaction_create_t::get_parameter - // create a fake interaction - dpp::cluster cluster(""); - dpp::discord_client client(&cluster, 1, 1, ""); - dpp::interaction_create_t interaction(&client, ""); - - /* Check the method with subcommands */ - set_test(GET_PARAMETER_WITH_SUBCOMMANDS, false); - - dpp::command_interaction cmd_data; // command - cmd_data.type = dpp::ctxm_chat_input; - cmd_data.name = "command"; - - dpp::command_data_option subcommandgroup; // subcommand group - subcommandgroup.name = "group"; - subcommandgroup.type = dpp::co_sub_command_group; - - dpp::command_data_option subcommand; // subcommand - subcommand.name = "add"; - subcommand.type = dpp::co_sub_command; - - dpp::command_data_option option1; // slashcommand option - option1.name = "user"; - option1.type = dpp::co_user; - option1.value = dpp::snowflake(189759562910400512); - - dpp::command_data_option option2; // slashcommand option - option2.name = "checked"; - option2.type = dpp::co_boolean; - option2.value = true; - - // add them - subcommand.options.push_back(option1); - subcommand.options.push_back(option2); - subcommandgroup.options.push_back(subcommand); - cmd_data.options.push_back(subcommandgroup); - interaction.command.data = cmd_data; - - dpp::snowflake value1 = std::get(interaction.get_parameter("user")); - set_test(GET_PARAMETER_WITH_SUBCOMMANDS, value1 == dpp::snowflake(189759562910400512)); - - /* Check the method without subcommands */ - set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, false); - - dpp::command_interaction cmd_data2; // command - cmd_data2.type = dpp::ctxm_chat_input; - cmd_data2.name = "command"; - - dpp::command_data_option option3; // slashcommand option - option3.name = "number"; - option3.type = dpp::co_integer; - option3.value = int64_t(123456); - - cmd_data2.options.push_back(option3); - interaction.command.data = cmd_data2; - - int64_t value2 = std::get(interaction.get_parameter("number")); - set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, value2 == 123456); - } - - { // test dpp::command_option_choice::fill_from_json - set_test(OPTCHOICE_DOUBLE, false); - set_test(OPTCHOICE_INT, false); - set_test(OPTCHOICE_BOOL, false); - set_test(OPTCHOICE_SNOWFLAKE, false); - set_test(OPTCHOICE_STRING, false); - json j; - dpp::command_option_choice choice; - j["value"] = 54.321; - choice.fill_from_json(&j); - bool success_double = std::holds_alternative(choice.value); - j["value"] = 8223372036854775807; - choice.fill_from_json(&j); - bool success_int = std::holds_alternative(choice.value); - j["value"] = -8223372036854775807; - choice.fill_from_json(&j); - bool success_int2 = std::holds_alternative(choice.value); - j["value"] = true; - choice.fill_from_json(&j); - bool success_bool = std::holds_alternative(choice.value); - dpp::snowflake s(845266178036516757); // example snowflake - j["value"] = s; - choice.fill_from_json(&j); - bool success_snowflake = std::holds_alternative(choice.value) && std::get(choice.value) == s; - j["value"] = "foobar"; - choice.fill_from_json(&j); - bool success_string = std::holds_alternative(choice.value); - set_test(OPTCHOICE_DOUBLE, success_double); - set_test(OPTCHOICE_INT, success_int && success_int2); - set_test(OPTCHOICE_BOOL, success_bool); - set_test(OPTCHOICE_SNOWFLAKE, success_snowflake); - set_test(OPTCHOICE_STRING, success_string); - } - - { // test permissions - set_test(PERMISSION_CLASS, false); - bool success = false; - auto p = dpp::permission(); - p = 16; - success = p == 16; - p |= 4; - success = p == 20 && success; - p <<= 8; // left shift - success = p == 5120 && success; - auto s = std::to_string(p); - success = s == "5120" && success; - p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_administrator); - success = !p.has(dpp::p_administrator, dpp::p_ban_members) && success; // must return false because they're not both set - success = !p.has(dpp::p_administrator | dpp::p_ban_members) && success; - success = p.can(dpp::p_ban_members) && success; - success = p.can(dpp::p_speak) && success; - - constexpr auto permission_test = [](dpp::permission p) constexpr noexcept { - bool success{true}; - - p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_connect); - p.set(dpp::p_administrator, dpp::p_ban_members); - success = p.has(dpp::p_administrator) && success; - success = p.has(dpp::p_administrator) && p.has(dpp::p_ban_members) && success; - success = p.has(dpp::p_administrator, dpp::p_ban_members) && success; - success = p.has(dpp::p_administrator | dpp::p_ban_members) && success; - success = p.add(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; - success = !p.remove(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; - p.remove(dpp::p_administrator); - success = p.can(dpp::p_ban_members) && success; - success = !p.can(dpp::p_speak, dpp::p_ban_members) && success; - success = p.can_any(dpp::p_speak, dpp::p_ban_members) && success; - return success; - }; - constexpr auto constexpr_success = permission_test({~uint64_t{0}}); // test in constant evaluated - success = permission_test({~uint64_t{0}}) && constexpr_success && success; // test at runtime - set_test(PERMISSION_CLASS, success); - } - - { // some dpp::user methods - dpp::user user1; - user1.id = 189759562910400512; - user1.discriminator = 0001; - user1.username = "brain"; - - set_test(USER_GET_MENTION, false); - set_test(USER_GET_MENTION, user1.get_mention() == "<@189759562910400512>"); - - set_test(USER_FORMAT_USERNAME, false); - set_test(USER_FORMAT_USERNAME, user1.format_username() == "brain#0001"); - - set_test(USER_GET_CREATION_TIME, false); - set_test(USER_GET_CREATION_TIME, (uint64_t)user1.get_creation_time() == 1465312605); - - set_test(USER_GET_URL, false); - - dpp::user user2; - set_test(USER_GET_URL, - user1.get_url() == dpp::utility::url_host + "/users/189759562910400512" && - user2.get_url() == "" - ); - } - - { // avatar size function - set_test(UTILITY_AVATAR_SIZE, false); - bool success = false; - success = dpp::utility::avatar_size(0).empty(); - success = dpp::utility::avatar_size(16) == "?size=16" && success; - success = dpp::utility::avatar_size(256) == "?size=256" && success; - success = dpp::utility::avatar_size(4096) == "?size=4096" && success; - success = dpp::utility::avatar_size(8192).empty() && success; - success = dpp::utility::avatar_size(3000).empty() && success; - set_test(UTILITY_AVATAR_SIZE, success); - } - - // some dpp::role test - set_test(ROLE_COMPARE, false); - dpp::role role_1, role_2; - role_1.position = 1; - role_2.position = 2; - set_test(ROLE_COMPARE, role_1 < role_2 && role_1 != role_2); - - { // message methods - dpp::message m; - m.guild_id = 825407338755653642; - m.channel_id = 956230231277072415; - m.id = 1151617986541666386; - - dpp::message m2; - m2.guild_id = 825407338755653642; - m2.channel_id = 956230231277072415; - - dpp::message m3; - m3.guild_id = 825407338755653642; - m3.id = 1151617986541666386; - - dpp::message m4; - m4.channel_id = 956230231277072415; - m4.id = 1151617986541666386; - - dpp::message m5; - m5.guild_id = 825407338755653642; - - dpp::message m6; - m6.channel_id = 956230231277072415; - - dpp::message m7; - m7.id = 1151617986541666386; - - dpp::message m8; - - set_test(MESSAGE_GET_URL, false); - set_test(MESSAGE_GET_URL, - m.get_url() == dpp::utility::url_host + "/channels/825407338755653642/956230231277072415/1151617986541666386" && - m2.get_url() == "" && - m3.get_url() == "" && - m4.get_url() == "" && - m5.get_url() == "" && - m6.get_url() == "" && - m7.get_url() == "" && - m8.get_url() == "" - ); - } - - { // channel methods - set_test(CHANNEL_SET_TYPE, false); - dpp::channel c; - c.set_flags(dpp::c_nsfw | dpp::c_video_quality_720p); - c.set_type(dpp::CHANNEL_CATEGORY); - bool before = c.is_category() && !c.is_forum(); - c.set_type(dpp::CHANNEL_FORUM); - bool after = !c.is_category() && c.is_forum(); - set_test(CHANNEL_SET_TYPE, before && after); - - set_test(CHANNEL_GET_MENTION, false); - c.id = 825411707521728511; - set_test(CHANNEL_GET_MENTION, c.get_mention() == "<#825411707521728511>"); - - set_test(CHANNEL_GET_URL, false); - c.guild_id = 825407338755653642; - - dpp::channel c2; - c2.id = 825411707521728511; - - dpp::channel c3; - c3.guild_id = 825407338755653642; - - dpp::channel c4; - - set_test(CHANNEL_GET_URL, - c.get_url() == dpp::utility::url_host + "/channels/825407338755653642/825411707521728511" && - c2.get_url() == "" && - c3.get_url() == "" && - c4.get_url() == "" - ); - } - - { // cdn endpoint url getter - set_test(UTILITY_CDN_ENDPOINT_URL_HASH, false); - bool success = false; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_jpg, 0).empty(); - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 0) == "https://cdn.discordapp.com/foobar/test.png" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 128) == "https://cdn.discordapp.com/foobar/test.png?size=128" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_gif, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.png" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, false) == "https://cdn.discordapp.com/foobar/test/12345.png" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test.gif" && success; - success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_gif, 0, false, false).empty() && success; - set_test(UTILITY_CDN_ENDPOINT_URL_HASH, success); - } - - { // user url getter - dpp::user user1; - user1.id = 189759562910400512; - user1.username = "Brain"; - user1.discriminator = 0001; - - auto user2 = user1; - user2.avatar = "5532c6414c70765a28cf9448c117205f"; - - auto user3 = user2; - user3.flags |= dpp::u_animated_icon; - - set_test(USER_GET_AVATAR_URL, false); - set_test(USER_GET_AVATAR_URL, - dpp::user().get_avatar_url().empty() && - user1.get_avatar_url() == dpp::utility::cdn_host + "/embed/avatars/1.png" && - user2.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.png" && - user2.get_avatar_url(0, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.webp" && - user2.get_avatar_url(0, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.jpg" && - user3.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" && - user3.get_avatar_url(4096, dpp::i_gif) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=4096" && - user3.get_avatar_url(512, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && - user3.get_avatar_url(512, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && - user3.get_avatar_url(16, dpp::i_jpg, false) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.jpg?size=16" && - user3.get_avatar_url(5000) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" - ); - } - - { // sticker url getter - set_test(STICKER_GET_URL, false); - dpp::sticker s; - s.format_type = dpp::sf_png; - bool success = s.get_url().empty(); - s.id = 12345; - success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.png" && success; - s.format_type = dpp::sf_gif; - success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.gif" && success; - s.format_type = dpp::sf_lottie; - success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.json" && success; - set_test(STICKER_GET_URL, success); - } - - { // emoji url getter - dpp::emoji emoji; - emoji.id = 825407338755653641; - - set_test(EMOJI_GET_URL, false); - set_test(EMOJI_GET_URL, emoji.get_url() == dpp::utility::cdn_host + "/emojis/825407338755653641.png"); - } - - { // utility methods - set_test(UTILITY_GUILD_NAVIGATION, false); - auto gn1 = dpp::utility::guild_navigation(123, dpp::utility::gnt_customize); - auto gn2 = dpp::utility::guild_navigation(1234, dpp::utility::gnt_browse); - auto gn3 = dpp::utility::guild_navigation(12345, dpp::utility::gnt_guide); - set_test(UTILITY_GUILD_NAVIGATION, gn1 == "<123:customize>" && gn2 == "<1234:browse>" && gn3 == "<12345:guide>"); - - set_test(UTILITY_ICONHASH, false); - auto iconhash1 = dpp::utility::iconhash("a_5532c6414c70765a28cf9448c117205f"); - set_test(UTILITY_ICONHASH, iconhash1.first == 6139187225817019994 && - iconhash1.second == 2940732121894297695 && - iconhash1.to_string() == "5532c6414c70765a28cf9448c117205f" - ); - - set_test(UTILITY_MAKE_URL_PARAMETERS, false); - auto url_params1 = dpp::utility::make_url_parameters({ - {"foo", 15}, - {"bar", 7} - }); - auto url_params2 = dpp::utility::make_url_parameters({ - {"foo", "hello"}, - {"bar", "two words"} - }); - set_test(UTILITY_MAKE_URL_PARAMETERS, url_params1 == "?bar=7&foo=15" && url_params2 == "?bar=two%20words&foo=hello"); - - set_test(UTILITY_MARKDOWN_ESCAPE, false); - auto escaped = dpp::utility::markdown_escape( - "> this is a quote\n" - "**some bold text**"); - set_test(UTILITY_MARKDOWN_ESCAPE, "\\>this is a quote\\n\\*\\*some bold text\\*\\*"); - - set_test(UTILITY_TOKENIZE, false); - auto tokens = dpp::utility::tokenize("some Whitespace seperated Text to Tokenize", " "); - std::vector expected_tokens = {"some", "Whitespace", "seperated", "Text", "to", "Tokenize"}; - set_test(UTILITY_TOKENIZE, tokens == expected_tokens); - - set_test(UTILITY_URL_ENCODE, false); - auto url_encoded = dpp::utility::url_encode("S2-^$1Nd+U!g'8+_??o?p-bla bla"); - set_test(UTILITY_URL_ENCODE, url_encoded == "S2-%5E%241Nd%2BU%21g%278%2B_%3F%3Fo%3Fp-bla%20bla"); - - set_test(UTILITY_SLASHCOMMAND_MENTION, false); - auto mention1 = dpp::utility::slashcommand_mention(123, "name"); - auto mention2 = dpp::utility::slashcommand_mention(123, "name", "sub"); - auto mention3 = dpp::utility::slashcommand_mention(123, "name", "group", "sub"); - bool success = mention1 == "" && mention2 == "" && mention3 == ""; - set_test(UTILITY_SLASHCOMMAND_MENTION, success); - - set_test(UTILITY_CHANNEL_MENTION, false); - auto channel_mention = dpp::utility::channel_mention(123); - set_test(UTILITY_CHANNEL_MENTION, channel_mention == "<#123>"); - - set_test(UTILITY_USER_MENTION, false); - auto user_mention = dpp::utility::user_mention(123); - set_test(UTILITY_USER_MENTION, user_mention == "<@123>"); - - set_test(UTILITY_ROLE_MENTION, false); - auto role_mention = dpp::utility::role_mention(123); - set_test(UTILITY_ROLE_MENTION, role_mention == "<@&123>"); - - set_test(UTILITY_EMOJI_MENTION, false); - auto emoji_mention1 = dpp::utility::emoji_mention("role1", 123, false); - auto emoji_mention2 = dpp::utility::emoji_mention("role2", 234, true); - auto emoji_mention3 = dpp::utility::emoji_mention("white_check_mark", 0, false); - auto emoji_mention4 = dpp::utility::emoji_mention("white_check_mark", 0, true); - set_test(UTILITY_EMOJI_MENTION, - emoji_mention1 == "<:role1:123>" && - emoji_mention2 == "" && - emoji_mention3 == ":white_check_mark:" && - emoji_mention4 == ":white_check_mark:" - ); - - set_test(UTILITY_USER_URL, false); - auto user_url = dpp::utility::user_url(123); - set_test(UTILITY_USER_URL, - user_url == dpp::utility::url_host + "/users/123" && - dpp::utility::user_url(0) == "" - ); - - set_test(UTILITY_MESSAGE_URL, false); - auto message_url = dpp::utility::message_url(1,2,3); - set_test(UTILITY_MESSAGE_URL, - message_url == dpp::utility::url_host+ "/channels/1/2/3" && - dpp::utility::message_url(0,2,3) == "" && - dpp::utility::message_url(1,0,3) == "" && - dpp::utility::message_url(1,2,0) == "" && - dpp::utility::message_url(0,0,3) == "" && - dpp::utility::message_url(0,2,0) == "" && - dpp::utility::message_url(1,0,0) == "" && - dpp::utility::message_url(0,0,0) == "" - ); - - set_test(UTILITY_CHANNEL_URL, false); - auto channel_url = dpp::utility::channel_url(1,2); - set_test(UTILITY_CHANNEL_URL, - channel_url == dpp::utility::url_host+ "/channels/1/2" && - dpp::utility::channel_url(0,2) == "" && - dpp::utility::channel_url(1,0) == "" && - dpp::utility::channel_url(0,0) == "" - ); - - set_test(UTILITY_THREAD_URL, false); - auto thread_url = dpp::utility::thread_url(1,2); - set_test(UTILITY_THREAD_URL, - thread_url == dpp::utility::url_host+ "/channels/1/2" && - dpp::utility::thread_url(0,2) == "" && - dpp::utility::thread_url(1,0) == "" && - dpp::utility::thread_url(0,0) == "" - ); - } - - { // dpp event classes - start_test(EVENT_CLASS); - bool success = true; - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::log_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_execute_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_state_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::interaction_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::slashcommand_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::button_click_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::form_submit_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::autocomplete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::context_menu_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_context_menu_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_context_menu_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::select_click_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_stickers_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_join_request_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::ready_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::resumed_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::typing_start_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_track_marker_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_members_chunk_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_emoji_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_bulk_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_pins_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_all_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_server_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_emojis_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::presence_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::webhooks_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_integrations_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_audit_log_entry_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_add_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_remove_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_create_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_delete_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_list_sync_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_member_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_members_update_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_buffer_send_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_user_talking_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_ready_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_receive_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_speaking_t, success); - DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_disconnect_t, success); - set_test(EVENT_CLASS, success); - } -} diff --git a/src/unittest/errors.cpp b/src/unittest/errors.cpp deleted file mode 100644 index 8584777eaf..0000000000 --- a/src/unittest/errors.cpp +++ /dev/null @@ -1,118 +0,0 @@ -/************************************************************************************ - * - * D++, A Lightweight C++ library for Discord - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2021 Craig Edwards and D++ contributors - * (https://github.com/brainboxdotcc/DPP/graphs/contributors) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ************************************************************************************/ -#include "test.h" - -/* Unit tests for human-readable error translation */ -void errors_test() { - set_test(ERRORS, false); - - /* Prepare a confirmation_callback_t in error state (400) */ - dpp::confirmation_callback_t error_test; - bool error_message_success = false; - error_test.http_info.status = 400; - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"options\": {\ - \"0\": {\ - \"name\": {\ - \"_errors\": [\ - {\ - \"code\": \"STRING_TYPE_REGEX\",\ - \"message\": \"String value did not match validation regex.\"\ - },\ - {\ - \"code\": \"APPLICATION_COMMAND_INVALID_NAME\",\ - \"message\": \"Command name is invalid\"\ - }\ - ]\ - }\ - }\ - }\ - }\ - }"; - error_message_success = (error_test.get_error().human_readable == "50035: Invalid Form Body\n\t- options[0].name: String value did not match validation regex. (STRING_TYPE_REGEX)\n\t- options[0].name: Command name is invalid (APPLICATION_COMMAND_INVALID_NAME)"); - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"type\": {\ - \"_errors\": [\ - {\ - \"code\": \"BASE_TYPE_CHOICES\",\ - \"message\": \"Value must be one of {4, 5, 9, 10, 11}.\"\ - }\ - ]\ - }\ - }\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - type: Value must be one of {4, 5, 9, 10, 11}. (BASE_TYPE_CHOICES)"); - - error_test.http_info.body = "{\ - \"message\": \"Unknown Guild\",\ - \"code\": 10004\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "10004: Unknown Guild"); - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"allowed_mentions\": {\ - \"_errors\": [\ - {\ - \"code\": \"MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE\",\ - \"message\": \"parse:[\\\"users\\\"] and users: [ids...] are mutually exclusive.\"\ - }\ - ]\ - }\ - }\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - allowed_mentions: parse:[\"users\"] and users: [ids...] are mutually exclusive. (MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE)"); - - error_test.http_info.body = "{\ - \"message\": \"Invalid Form Body\",\ - \"code\": 50035,\ - \"errors\": {\ - \"1\": {\ - \"options\": {\ - \"1\": {\ - \"description\": {\ - \"_errors\": [\ - {\ - \"code\": \"BASE_TYPE_BAD_LENGTH\",\ - \"message\": \"Must be between 1 and 100 in length.\"\ - }\ - ]\ - }\ - }\ - }\ - }\ - }\ - }"; - error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - [1].options[1].description: Must be between 1 and 100 in length. (BASE_TYPE_BAD_LENGTH)"); - - set_test(ERRORS, error_message_success); -} diff --git a/src/unittest/gateway_events.cpp b/src/unittest/gateway_events.cpp deleted file mode 100644 index beddfbe89f..0000000000 --- a/src/unittest/gateway_events.cpp +++ /dev/null @@ -1,1296 +0,0 @@ -/************************************************************************************ - * - * D++, A Lightweight C++ library for Discord - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2021 Craig Edwards and D++ contributors - * (https://github.com/brainboxdotcc/DPP/graphs/contributors) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ************************************************************************************/ -#include "test.h" - -#include -#include - -/* Unit tests for Gateway events */ -void gateway_events_tests(const std::string& token, dpp::cluster& bot) { - std::vector test_image = load_test_image(); - std::vector testaudio = load_test_audio(); - - set_test(PRESENCE, false); - set_test(CLUSTER, false); - try { - bot.set_websocket_protocol(dpp::ws_etf); - set_test(CLUSTER, true); - set_test(CONNECTION, false); - set_test(GUILDCREATE, false); - set_test(ICONHASH, false); - - set_test(MSGCOLLECT, false); - if (!offline) { - /* Intentional leak: freed on unit test end */ - [[maybe_unused]] - message_collector* collect_messages = new message_collector(&bot, 25); - } - - set_test(JSON_PARSE_ERROR, false); - dpp::rest_request(&bot, "/nonexistent", "address", "", dpp::m_get, "", [](const dpp::confirmation_callback_t& e) { - if (e.is_error() && e.get_error().code == 404) { - set_test(JSON_PARSE_ERROR, true); - } else { - set_test(JSON_PARSE_ERROR, false); - } - }); - - dpp::utility::iconhash i; - std::string dummyval("fcffffffffffff55acaaaaaaaaaaaa66"); - i = dummyval; - set_test(ICONHASH, (i.to_string() == dummyval)); - - /* This ensures we test both protocols, as voice is json and shard is etf */ - bot.set_websocket_protocol(dpp::ws_etf); - - bot.on_form_submit([&](const dpp::form_submit_t & event) { - }); - - /* This is near impossible to test without a 'clean room' voice channel. - * We attach this event just so that the decoder events are fired while we - * are sending audio later, this way if the audio receive code is plain unstable - * the test suite will crash and fail. - */ - bot.on_voice_receive_combined([&](const auto& event) { - }); - - std::promise ready_promise; - std::future ready_future = ready_promise.get_future(); - bot.on_ready([&](const dpp::ready_t & event) { - set_test(CONNECTION, true); - ready_promise.set_value(); - - set_test(APPCOMMAND, false); - set_test(LOGGER, false); - bot.log(dpp::ll_info, "Test log message"); - - bot.guild_command_create(dpp::slashcommand().set_name("testcommand") - .set_description("Test command for DPP unit test") - .add_option(dpp::command_option(dpp::co_attachment, "file", "a file")) - .set_application_id(bot.me.id) - .add_localization("fr", "zut", "Ou est la salor dans Discord?"), - TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(APPCOMMAND, true); - set_test(DELCOMMAND, false); - dpp::slashcommand s = std::get(callback.value); - bot.guild_command_delete(s.id, TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - dpp::message test_message(TEST_TEXT_CHANNEL_ID, "test message"); - - set_test(DELCOMMAND, true); - set_test(MESSAGECREATE, false); - set_test(MESSAGEEDIT, false); - set_test(MESSAGERECEIVE, false); - test_message.add_file("no-mime", "test"); - test_message.add_file("test.txt", "test", "text/plain"); - test_message.add_file("test.png", std::string{test_image.begin(), test_image.end()}, "image/png"); - bot.message_create(test_message, [&bot](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(MESSAGECREATE, true); - set_test(REACT, false); - dpp::message m = std::get(callback.value); - set_test(REACTEVENT, false); - bot.message_add_reaction(m.id, TEST_TEXT_CHANNEL_ID, "😄", [](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(REACT, true); - } else { - set_test(REACT, false); - } - }); - set_test(EDITEVENT, false); - bot.message_edit(dpp::message(m).set_content("test edit"), [](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(MESSAGEEDIT, true); - } - }); - } - }); - } else { - set_test(DELCOMMAND, false); - } - }); - } - }); - }); - - std::mutex loglock; - bot.on_log([&](const dpp::log_t & event) { - std::lock_guard locker(loglock); - if (event.severity > dpp::ll_trace) { - std::cout << "[" << std::fixed << std::setprecision(3) << (dpp::utility::time_f() - get_start_time()) << "]: [\u001b[36m" << dpp::utility::loglevel(event.severity) << "\u001b[0m] " << event.message << "\n"; - } - if (event.message == "Test log message") { - set_test(LOGGER, true); - } - }); - - set_test(RUNONCE, false); - uint8_t runs = 0; - for (int x = 0; x < 10; ++x) { - if (dpp::run_once()) { - runs++; - } - } - set_test(RUNONCE, (runs == 1)); - - bot.on_voice_ready([&](const dpp::voice_ready_t & event) { - set_test(VOICECONN, true); - dpp::discord_voice_client* v = event.voice_client; - set_test(VOICESEND, false); - if (v && v->is_ready()) { - v->send_audio_raw(reinterpret_cast(testaudio.data()), testaudio.size()); // TODO: fix type punning - } else { - set_test(VOICESEND, false); - } - }); - - bot.on_invite_create([](const dpp::invite_create_t &event) { - auto &inv = event.created_invite; - if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID && inv.created_at != 0 && inv.max_uses == 100) { - set_test(INVITE_CREATE_EVENT, true); - } - }); - - bot.on_invite_delete([](const dpp::invite_delete_t &event) { - auto &inv = event.deleted_invite; - if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID) { - set_test(INVITE_DELETE_EVENT, true); - } - }); - - bot.on_voice_buffer_send([&](const dpp::voice_buffer_send_t & event) { - if (event.buffer_size == 0) { - set_test(VOICESEND, true); - } - }); - - set_test(SYNC, false); - if (!offline) { - dpp::message m = dpp::sync(&bot, &dpp::cluster::message_create, dpp::message(TEST_TEXT_CHANNEL_ID, "TEST")); - set_test(SYNC, m.content == "TEST"); - } - - bot.on_guild_create([&](const dpp::guild_create_t & event) { - if (event.created->id == TEST_GUILD_ID) { - set_test(GUILDCREATE, true); - if (event.presences.size() && event.presences.begin()->second.user_id > 0) { - set_test(PRESENCE, true); - } - dpp::guild* g = dpp::find_guild(TEST_GUILD_ID); - set_test(CACHE, false); - if (g) { - set_test(CACHE, true); - set_test(VOICECONN, false); - dpp::discord_client* s = bot.get_shard(0); - s->connect_voice(g->id, TEST_VC_ID, false, false); - } - else { - set_test(CACHE, false); - } - } - }); - - // this helper class contains logic for the message tests, deletes the message when all tests are done - class message_test_helper { - private: - std::mutex mutex; - bool pin_tested = false; - bool thread_tested = false; - std::array files_tested{}; - std::array files_success{}; - dpp::snowflake channel_id; - dpp::snowflake message_id; - dpp::cluster ⊥ - - void delete_message_if_done() { - if (files_tested == std::array{true, true, true} && pin_tested && thread_tested) { - set_test(MESSAGEDELETE, false); - bot.message_delete(message_id, channel_id, [](const dpp::confirmation_callback_t &callback) { - if (!callback.is_error()) { - set_test(MESSAGEDELETE, true); - } - }); - } - } - - void set_pin_tested() { - assert(!pin_tested); - pin_tested = true; - delete_message_if_done(); - } - - void set_thread_tested() { - assert(!thread_tested); - thread_tested = true; - delete_message_if_done(); - } - - void set_file_tested(size_t index) { - assert(!files_tested[index]); - files_tested[index] = true; - if (files_tested == std::array{true, true, true}) { - set_test(MESSAGEFILE, files_success == std::array{true, true, true}); - } - delete_message_if_done(); - } - - void test_threads(const dpp::message &message) { - set_test(THREAD_CREATE_MESSAGE, false); - set_test(THREAD_DELETE, false); - set_test(THREAD_DELETE_EVENT, false); - bot.thread_create_with_message("test", message.channel_id, message.id, 60, 60, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (callback.is_error()) { - set_thread_tested(); - } - else { - auto thread = callback.get(); - thread_id = thread.id; - set_test(THREAD_CREATE_MESSAGE, true); - bot.channel_delete(thread.id, [this](const dpp::confirmation_callback_t &callback) { - set_test(THREAD_DELETE, !callback.is_error()); - set_thread_tested(); - }); - } - }); - } - - void test_files(const dpp::message &message) { - set_test(MESSAGEFILE, false); - if (message.attachments.size() == 3) { - static constexpr auto check_mimetype = [](const auto &headers, std::string mimetype) { - if (auto it = headers.find("content-type"); it != headers.end()) { - // check that the mime type starts with what we gave : for example discord will change "text/plain" to "text/plain; charset=UTF-8" - return it->second.size() >= mimetype.size() && std::equal(it->second.begin(), it->second.begin() + mimetype.size(), mimetype.begin()); - } - else { - return false; - } - }; - message.attachments[0].download([&](const dpp::http_request_completion_t &callback) { - std::lock_guard lock(mutex); - if (callback.status == 200 && callback.body == "test") { - files_success[0] = true; - } - set_file_tested(0); - }); - message.attachments[1].download([&](const dpp::http_request_completion_t &callback) { - std::lock_guard lock(mutex); - if (callback.status == 200 && check_mimetype(callback.headers, "text/plain") && callback.body == "test") { - files_success[1] = true; - } - set_file_tested(1); - }); - message.attachments[2].download([&](const dpp::http_request_completion_t &callback) { - std::lock_guard lock(mutex); - // do not check the contents here because discord can change compression - if (callback.status == 200 && check_mimetype(callback.headers, "image/png")) { - files_success[2] = true; - } - set_file_tested(2); - }); - } - else { - set_file_tested(0); - set_file_tested(1); - set_file_tested(2); - } - } - - void test_pin() { - if (!extended) { - set_pin_tested(); - return; - } - set_test(MESSAGEPIN, false); - set_test(MESSAGEUNPIN, false); - bot.message_pin(channel_id, message_id, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (!callback.is_error()) { - set_test(MESSAGEPIN, true); - bot.message_unpin(TEST_TEXT_CHANNEL_ID, message_id, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (!callback.is_error()) { - set_test(MESSAGEUNPIN, true); - } - set_pin_tested(); - }); - } - else { - set_pin_tested(); - } - }); - } - - public: - dpp::snowflake thread_id; - - explicit message_test_helper(dpp::cluster &_bot) : bot(_bot) {} - - void run(const dpp::message &message) { - pin_tested = false; - thread_tested = false; - files_tested = {false, false, false}; - files_success = {false, false, false}; - channel_id = message.channel_id; - message_id = message.id; - test_pin(); - test_files(message); - test_threads(message); - } - }; - - message_test_helper message_helper(bot); - - class thread_test_helper { - public: - enum event_flag { - MESSAGE_CREATE = 1 << 0, - MESSAGE_EDIT = 1 << 1, - MESSAGE_REACT = 1 << 2, - MESSAGE_REMOVE_REACT = 1 << 3, - MESSAGE_DELETE = 1 << 4, - EVENT_END = 1 << 5 - }; - private: - std::mutex mutex; - dpp::cluster ⊥ - bool edit_tested = false; - bool members_tested = false; - bool messages_tested = false; - bool events_tested = false; - bool get_active_tested = false; - uint32_t events_tested_mask = 0; - uint32_t events_to_test_mask = 0; - - void delete_if_done() { - if (edit_tested && members_tested && messages_tested && events_tested && get_active_tested) { - bot.channel_delete(thread_id); - } - } - - void set_events_tested() { - if (events_tested) { - return; - } - events_tested = true; - delete_if_done(); - } - - void set_edit_tested() { - if (edit_tested) { - return; - } - edit_tested = true; - delete_if_done(); - } - - void set_members_tested() { - if (members_tested) { - return; - } - members_tested = true; - delete_if_done(); - } - - void set_get_active_tested() { - if (get_active_tested) { - return; - } - get_active_tested = true; - delete_if_done(); - } - - void set_messages_tested() { - if (messages_tested) { - return; - } - messages_tested = true; - delete_if_done(); - } - - void set_event_tested(event_flag flag) { - if (events_tested_mask & flag) { - return; - } - events_tested_mask |= flag; - for (uint32_t i = 1; i < EVENT_END; i <<= 1) { - if ((events_to_test_mask & i) && (events_tested_mask & i) != i) { - return; - } - } - set_events_tested(); - } - - void events_abort() { - events_tested_mask |= ~events_to_test_mask; - for (uint32_t i = 1; i < EVENT_END; i <<= 1) { - if ((events_tested_mask & i) != i) { - return; - } - } - set_events_tested(); - } - - public: - /** - * @Brief wrapper for set_event_tested, locking the mutex. Meant to be used from outside the class - */ - void notify_event_tested(event_flag flag) { - std::lock_guard lock{mutex}; - - set_event_tested(flag); - } - - dpp::snowflake thread_id; - - void test_edit(const dpp::thread &thread) { - std::lock_guard lock{mutex}; - - if (!edit_tested) { - dpp::thread edit = thread; - set_test(THREAD_EDIT, false); - set_test(THREAD_UPDATE_EVENT, false); - edit.name = "edited"; - edit.metadata.locked = true; - bot.thread_edit(edit, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock(mutex); - if (!callback.is_error()) { - set_test(THREAD_EDIT, true); - } - set_edit_tested(); - }); - } - } - - void test_get_active(const dpp::thread &thread) { - std::lock_guard lock{mutex}; - - set_test(THREAD_GET_ACTIVE, false); - bot.threads_get_active(TEST_GUILD_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (!callback.is_error()) { - const auto &threads = callback.get(); - if (auto thread_it = threads.find(thread_id); thread_it != threads.end()) { - const auto &thread = thread_it->second.active_thread; - const auto &member = thread_it->second.bot_member; - if (thread.id == thread_id && member.has_value() && member->user_id == bot.me.id) { - set_test(THREAD_GET_ACTIVE, true); - } - } - } - set_get_active_tested(); - }); - } - - void test_members(const dpp::thread &thread) { - std::lock_guard lock{mutex}; - - if (!members_tested) { - if (!extended) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBER_ADD, false); - set_test(THREAD_MEMBER_GET, false); - set_test(THREAD_MEMBERS_GET, false); - set_test(THREAD_MEMBER_REMOVE, false); - set_test(THREAD_MEMBERS_ADD_EVENT, false); - set_test(THREAD_MEMBERS_REMOVE_EVENT, false); - bot.thread_member_add(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBER_ADD, true); - bot.thread_member_get(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBER_GET, true); - bot.thread_members_get(thread_id, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - set_members_tested(); - return; - } - const auto &members = callback.get(); - if (members.find(TEST_USER_ID) == members.end() || members.find(bot.me.id) == members.end()) { - set_members_tested(); - return; - } - set_test(THREAD_MEMBERS_GET, true); - bot.thread_member_remove(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (!callback.is_error()) { - set_test(THREAD_MEMBER_REMOVE, true); - } - set_members_tested(); - }); - }); - }); - }); - } - } - - void test_messages(const dpp::thread &thread) { - if (!extended) { - set_messages_tested(); - set_events_tested(); - return; - } - - std::lock_guard lock{mutex}; - set_test(THREAD_MESSAGE, false); - set_test(THREAD_MESSAGE_CREATE_EVENT, false); - set_test(THREAD_MESSAGE_EDIT_EVENT, false); - set_test(THREAD_MESSAGE_REACT_ADD_EVENT, false); - set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, false); - set_test(THREAD_MESSAGE_DELETE_EVENT, false); - events_to_test_mask |= MESSAGE_CREATE; - bot.message_create(dpp::message{"hello thread"}.set_channel_id(thread.id), [this](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - auto m = callback.get(); - m.content = "hello thread?"; - events_to_test_mask |= MESSAGE_EDIT; - bot.message_edit(m, [this, message_id = m.id](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - events_to_test_mask |= MESSAGE_REACT; - bot.message_add_reaction(message_id, thread_id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - events_to_test_mask |= MESSAGE_REMOVE_REACT; - bot.message_delete_reaction(message_id, thread_id, bot.me.id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - if (callback.is_error()) { - events_abort(); - set_messages_tested(); - return; - } - events_to_test_mask |= MESSAGE_DELETE; - bot.message_delete(message_id, thread_id, [this] (const dpp::confirmation_callback_t &callback) { - std::lock_guard lock{mutex}; - set_messages_tested(); - if (callback.is_error()) { - events_abort(); - return; - } - set_test(THREAD_MESSAGE, true); - }); - }); - }); - }); - }); - } - - void run(const dpp::thread &thread) { - thread_id = thread.id; - test_get_active(thread); - test_edit(thread); - test_members(thread); - test_messages(thread); - } - - explicit thread_test_helper(dpp::cluster &bot_) : bot{bot_} - { - } - }; - - thread_test_helper thread_helper(bot); - - bot.on_thread_create([&](const dpp::thread_create_t &event) { - if (event.created.name == "thread test") { - set_test(THREAD_CREATE_EVENT, true); - thread_helper.run(event.created); - } - }); - - bool message_tested = false; - bot.on_message_create([&](const dpp::message_create_t & event) { - if (event.msg.author.id == bot.me.id) { - if (event.msg.content == "test message" && !message_tested) { - message_tested = true; - set_test(MESSAGERECEIVE, true); - message_helper.run(event.msg); - set_test(MESSAGESGET, false); - bot.messages_get(event.msg.channel_id, 0, event.msg.id, 0, 5, [](const dpp::confirmation_callback_t &cc){ - if (!cc.is_error()) { - dpp::message_map mm = std::get(cc.value); - if (mm.size()) { - set_test(MESSAGESGET, true); - set_test(TIMESTAMP, false); - dpp::message m = mm.begin()->second; - if (m.sent > 0) { - set_test(TIMESTAMP, true); - } else { - set_test(TIMESTAMP, false); - } - } else { - set_test(MESSAGESGET, false); - } - } else { - set_test(MESSAGESGET, false); - } - }); - set_test(MSGCREATESEND, false); - event.send("MSGCREATESEND", [&bot, ch_id = event.msg.channel_id] (const auto& cc) { - if (!cc.is_error()) { - dpp::message m = std::get(cc.value); - if (m.channel_id == ch_id) { - set_test(MSGCREATESEND, true); - } else { - bot.log(dpp::ll_debug, cc.http_info.body); - set_test(MSGCREATESEND, false); - } - bot.message_delete(m.id, m.channel_id); - } else { - bot.log(dpp::ll_debug, cc.http_info.body); - set_test(MSGCREATESEND, false); - } - }); - } - if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread") { - set_test(THREAD_MESSAGE_CREATE_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_CREATE); - } - } - }); - - bot.on_message_reaction_add([&](const dpp::message_reaction_add_t & event) { - if (event.reacting_user.id == bot.me.id) { - if (event.reacting_emoji.name == "😄") { - set_test(REACTEVENT, true); - } - if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { - set_test(THREAD_MESSAGE_REACT_ADD_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REACT); - } - } - }); - - bot.on_message_reaction_remove([&](const dpp::message_reaction_remove_t & event) { - if (event.reacting_user_id == bot.me.id) { - if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { - set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REMOVE_REACT); - } - } - }); - - bot.on_message_delete([&](const dpp::message_delete_t & event) { - if (event.channel_id == thread_helper.thread_id) { - set_test(THREAD_MESSAGE_DELETE_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_DELETE); - } - }); - - bool message_edit_tested = false; - bot.on_message_update([&](const dpp::message_update_t &event) { - if (event.msg.author == bot.me.id) { - if (event.msg.content == "test edit" && !message_edit_tested) { - message_edit_tested = true; - set_test(EDITEVENT, true); - } - if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread?") { - set_test(THREAD_MESSAGE_EDIT_EVENT, true); - thread_helper.notify_event_tested(thread_test_helper::MESSAGE_EDIT); - } - } - }); - - bot.on_thread_update([&](const dpp::thread_update_t &event) { - if (event.updating_guild->id == TEST_GUILD_ID && event.updated.id == thread_helper.thread_id && event.updated.name == "edited") { - set_test(THREAD_UPDATE_EVENT, true); - } - }); - - bot.on_thread_members_update([&](const dpp::thread_members_update_t &event) { - if (event.updating_guild->id == TEST_GUILD_ID && event.thread_id == thread_helper.thread_id) { - if (std::find_if(std::begin(event.added), std::end(event.added), is_owner) != std::end(event.added)) { - set_test(THREAD_MEMBERS_ADD_EVENT, true); - } - if (std::find_if(std::begin(event.removed_ids), std::end(event.removed_ids), is_owner) != std::end(event.removed_ids)) { - set_test(THREAD_MEMBERS_REMOVE_EVENT, true); - } - } - }); - - bot.on_thread_delete([&](const dpp::thread_delete_t &event) { - if (event.deleting_guild->id == TEST_GUILD_ID && event.deleted.id == message_helper.thread_id) { - set_test(THREAD_DELETE_EVENT, true); - } - }); - - // set to execute from this thread (main thread) after on_ready is fired - auto do_online_tests = [&] { - coro_online_tests(&bot); - set_test(GUILD_BAN_CREATE, false); - set_test(GUILD_BAN_GET, false); - set_test(GUILD_BANS_GET, false); - set_test(GUILD_BAN_DELETE, false); - if (!offline) { - // some deleted discord accounts to test the ban stuff with... - dpp::snowflake deadUser1(802670069523415057); - dpp::snowflake deadUser2(875302419335094292); - dpp::snowflake deadUser3(1048247361903792198); - - bot.set_audit_reason("ban reason one").guild_ban_add(TEST_GUILD_ID, deadUser1, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) bot.guild_ban_add(TEST_GUILD_ID, deadUser2, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) bot.set_audit_reason("ban reason three").guild_ban_add(TEST_GUILD_ID, deadUser3, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - set_test(GUILD_BAN_CREATE, true); - // when created, continue with getting and deleting - - // get ban - bot.guild_get_ban(TEST_GUILD_ID, deadUser1, [deadUser1](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - dpp::ban ban = event.get(); - if (ban.user_id == deadUser1 && ban.reason == "ban reason one") { - set_test(GUILD_BAN_GET, true); - } - } - }); - - // get multiple bans - bot.guild_get_bans(TEST_GUILD_ID, 0, deadUser1, 3, [deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - dpp::ban_map bans = event.get(); - int successCount = 0; - for (auto &ban: bans) { - if (ban.first == ban.second.user_id) { // the key should match the ban's user_id - if (ban.first == deadUser2 && ban.second.reason.empty()) { - successCount++; - } else if (ban.first == deadUser3 && ban.second.reason == "ban reason three") { - successCount++; - } - } - } - if (successCount == 2) { - set_test(GUILD_BANS_GET, true); - } - } - }); - - // unban them - bot.guild_ban_delete(TEST_GUILD_ID, deadUser1, [&bot, deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - bot.guild_ban_delete(TEST_GUILD_ID, deadUser2, [&bot, deadUser3](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - bot.guild_ban_delete(TEST_GUILD_ID, deadUser3, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(GUILD_BAN_DELETE, true); - } - }); - } - }); - } - }); - }); - }); - }); - } - - set_test(REQUEST_GET_IMAGE, false); - if (!offline) { - bot.request("https://dpp.dev/DPP-Logo.png", dpp::m_get, [&bot](const dpp::http_request_completion_t &callback) { - if (callback.status != 200) { - return; - } - set_test(REQUEST_GET_IMAGE, true); - - dpp::emoji emoji; - emoji.load_image(callback.body, dpp::i_png); - emoji.name = "dpp"; - - // emoji unit test with the requested image - set_test(EMOJI_CREATE, false); - set_test(EMOJI_GET, false); - set_test(EMOJI_DELETE, false); - bot.guild_emoji_create(TEST_GUILD_ID, emoji, [&bot](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - set_test(EMOJI_CREATE, true); - - auto created = event.get(); - bot.guild_emoji_get(TEST_GUILD_ID, created.id, [&bot, created](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto fetched = event.get(); - if (created.id == fetched.id && created.name == fetched.name && created.flags == fetched.flags) { - set_test(EMOJI_GET, true); - } - - bot.guild_emoji_delete(TEST_GUILD_ID, fetched.id, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(EMOJI_DELETE, true); - } - }); - }); - }); - }); - } - - set_test(INVITE_CREATE, false); - set_test(INVITE_GET, false); - set_test(INVITE_DELETE, false); - if (!offline) { - dpp::channel channel; - channel.id = TEST_TEXT_CHANNEL_ID; - dpp::invite invite; - invite.max_age = 0; - invite.max_uses = 100; - set_test(INVITE_CREATE_EVENT, false); - bot.channel_invite_create(channel, invite, [&bot, invite](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - - auto created = event.get(); - if (!created.code.empty() && created.channel_id == TEST_TEXT_CHANNEL_ID && created.guild_id == TEST_GUILD_ID && created.inviter.id == bot.me.id) { - set_test(INVITE_CREATE, true); - } - - bot.invite_get(created.code, [&bot, created](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - auto retrieved = event.get(); - if (retrieved.code == created.code && retrieved.guild_id == created.guild_id && retrieved.channel_id == created.channel_id && retrieved.inviter.id == created.inviter.id) { - if (retrieved.destination_guild.flags & dpp::g_community) { - set_test(INVITE_GET, retrieved.expires_at == 0); - } else { - set_test(INVITE_GET, true); - } - - } else { - set_test(INVITE_GET, false); - } - } else { - set_test(INVITE_GET, false); - } - - set_test(INVITE_DELETE_EVENT, false); - bot.invite_delete(created.code, [](const dpp::confirmation_callback_t &event) { - set_test(INVITE_DELETE, !event.is_error()); - }); - }); - }); - } - - set_test(AUTOMOD_RULE_CREATE, false); - set_test(AUTOMOD_RULE_GET, false); - set_test(AUTOMOD_RULE_GET_ALL, false); - set_test(AUTOMOD_RULE_DELETE, false); - if (!offline) { - dpp::automod_rule automodRule; - automodRule.name = "automod rule (keyword type)"; - automodRule.trigger_type = dpp::amod_type_keyword; - dpp::automod_metadata metadata1; - metadata1.keywords.emplace_back("*cat*"); - metadata1.keywords.emplace_back("train"); - metadata1.keywords.emplace_back("*.exe"); - metadata1.regex_patterns.emplace_back("^[^a-z]$"); - metadata1.allow_list.emplace_back("@silent*"); - automodRule.trigger_metadata = metadata1; - dpp::automod_action automodAction; - automodAction.type = dpp::amod_action_timeout; - automodAction.duration_seconds = 6000; - automodRule.actions.emplace_back(automodAction); - - bot.automod_rules_get(TEST_GUILD_ID, [&bot, automodRule](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto rules = event.get(); - set_test(AUTOMOD_RULE_GET_ALL, true); - for (const auto &rule: rules) { - if (rule.second.trigger_type == dpp::amod_type_keyword) { - // delete one automod rule of type KEYWORD before creating one to make space... - bot.automod_rule_delete(TEST_GUILD_ID, rule.first); - } - } - - // start creating the automod rules - bot.automod_rule_create(TEST_GUILD_ID, automodRule, [&bot, automodRule](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto created = event.get(); - if (created.name == automodRule.name) { - set_test(AUTOMOD_RULE_CREATE, true); - } - - // get automod rule - bot.automod_rule_get(TEST_GUILD_ID, created.id, [automodRule, &bot, created](const dpp::confirmation_callback_t &event) { - if (event.is_error()) { - return; - } - auto retrieved = event.get(); - if (retrieved.name == automodRule.name && - retrieved.trigger_type == automodRule.trigger_type && - retrieved.trigger_metadata.keywords == automodRule.trigger_metadata.keywords && - retrieved.trigger_metadata.regex_patterns == automodRule.trigger_metadata.regex_patterns && - retrieved.trigger_metadata.allow_list == automodRule.trigger_metadata.allow_list && retrieved.actions.size() == automodRule.actions.size()) { - set_test(AUTOMOD_RULE_GET, true); - } - - // delete the automod rule - bot.automod_rule_delete(TEST_GUILD_ID, retrieved.id, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(AUTOMOD_RULE_DELETE, true); - } - }); - }); - }); - }); - } - - set_test(USER_GET, false); - set_test(USER_GET_FLAGS, false); - if (!offline) { - bot.user_get(TEST_USER_ID, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - auto u = std::get(event.value); - if (u.id == TEST_USER_ID) { - set_test(USER_GET, true); - } else { - set_test(USER_GET, false); - } - json j = json::parse(event.http_info.body); - uint64_t raw_flags = j["public_flags"]; - if (j.contains("flags")) { - uint64_t flags = j["flags"]; - raw_flags |= flags; - } - // testing all user flags from https://discord.com/developers/docs/resources/user#user-object-user-flags - // they're manually set here because the dpp::user_flags don't match to the discord API, so we can't use them to compare with the raw flags! - if ( - u.is_discord_employee() == ((raw_flags & (1 << 0)) != 0) && - u.is_partnered_owner() == ((raw_flags & (1 << 1)) != 0) && - u.has_hypesquad_events() == ((raw_flags & (1 << 2)) != 0) && - u.is_bughunter_1() == ((raw_flags & (1 << 3)) != 0) && - u.is_house_bravery() == ((raw_flags & (1 << 6)) != 0) && - u.is_house_brilliance() == ((raw_flags & (1 << 7)) != 0) && - u.is_house_balance() == ((raw_flags & (1 << 8)) != 0) && - u.is_early_supporter() == ((raw_flags & (1 << 9)) != 0) && - u.is_team_user() == ((raw_flags & (1 << 10)) != 0) && - u.is_bughunter_2() == ((raw_flags & (1 << 14)) != 0) && - u.is_verified_bot() == ((raw_flags & (1 << 16)) != 0) && - u.is_verified_bot_dev() == ((raw_flags & (1 << 17)) != 0) && - u.is_certified_moderator() == ((raw_flags & (1 << 18)) != 0) && - u.is_bot_http_interactions() == ((raw_flags & (1 << 19)) != 0) && - u.is_active_developer() == ((raw_flags & (1 << 22)) != 0) - ) { - set_test(USER_GET_FLAGS, true); - } else { - set_test(USER_GET_FLAGS, false); - } - } else { - set_test(USER_GET, false); - set_test(USER_GET_FLAGS, false); - } - }); - } - - set_test(VOICE_CHANNEL_CREATE, false); - set_test(VOICE_CHANNEL_EDIT, false); - set_test(VOICE_CHANNEL_DELETE, false); - if (!offline) { - dpp::channel channel1; - channel1.set_type(dpp::CHANNEL_VOICE) - .set_guild_id(TEST_GUILD_ID) - .set_name("voice1") - .add_permission_overwrite(TEST_GUILD_ID, dpp::ot_role, 0, dpp::p_view_channel) - .set_user_limit(99); - dpp::channel createdChannel; - try { - createdChannel = bot.channel_create_sync(channel1); - } catch (dpp::rest_exception &exception) { - set_test(VOICE_CHANNEL_CREATE, false); - } - if (createdChannel.name == channel1.name && - createdChannel.user_limit == 99 && - createdChannel.name == "voice1") { - for (auto overwrite: createdChannel.permission_overwrites) { - if (overwrite.id == TEST_GUILD_ID && overwrite.type == dpp::ot_role && overwrite.deny == dpp::p_view_channel) { - set_test(VOICE_CHANNEL_CREATE, true); - } - } - - // edit the voice channel - createdChannel.set_name("foobar2"); - createdChannel.set_user_limit(2); - for (auto overwrite: createdChannel.permission_overwrites) { - if (overwrite.id == TEST_GUILD_ID) { - overwrite.deny.set(0); - overwrite.allow.set(dpp::p_view_channel); - } - } - try { - dpp::channel edited = bot.channel_edit_sync(createdChannel); - if (edited.name == "foobar2" && edited.user_limit == 2) { - set_test(VOICE_CHANNEL_EDIT, true); - } - } catch (dpp::rest_exception &exception) { - set_test(VOICE_CHANNEL_EDIT, false); - } - - // delete the voice channel - try { - bot.channel_delete_sync(createdChannel.id); - set_test(VOICE_CHANNEL_DELETE, true); - } catch (dpp::rest_exception &exception) { - set_test(VOICE_CHANNEL_DELETE, false); - } - } - } - - set_test(FORUM_CREATION, false); - set_test(FORUM_CHANNEL_GET, false); - set_test(FORUM_CHANNEL_DELETE, false); - if (!offline) { - dpp::channel c; - c.name = "test-forum-channel"; - c.guild_id = TEST_GUILD_ID; - c.set_topic("This is a forum channel"); - c.set_flags(dpp::CHANNEL_FORUM); - c.default_sort_order = dpp::so_creation_date; - dpp::forum_tag t; - t.name = "Alpha"; - t.emoji = "❌"; - c.available_tags = {t}; - c.default_auto_archive_duration = dpp::arc_1_day; - c.default_reaction = "✅"; - c.default_thread_rate_limit_per_user = 10; - bot.channel_create(c, [&bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(FORUM_CREATION, true); - auto channel = std::get(event.value); - // retrieve the forum channel and check the values - bot.channel_get(channel.id, [forum_id = channel.id, &bot](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - auto channel = std::get(event.value); - bot.log(dpp::ll_debug, event.http_info.body); - bool tag = false; - for (auto &t : channel.available_tags) { - if (t.name == "Alpha" && std::holds_alternative(t.emoji) && std::get(t.emoji) == "❌") { - tag = true; - } - } - bool name = channel.name == "test-forum-channel"; - bool sort = channel.default_sort_order == dpp::so_creation_date; - bool rateLimit = channel.default_thread_rate_limit_per_user == 10; - set_test(FORUM_CHANNEL_GET, tag && name && sort && rateLimit); - } else { - set_test(FORUM_CHANNEL_GET, false); - } - // delete the forum channel - bot.channel_delete(forum_id, [](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - set_test(FORUM_CHANNEL_DELETE, true); - } else { - set_test(FORUM_CHANNEL_DELETE, false); - } - }); - }); - } else { - set_test(FORUM_CREATION, false); - set_test(FORUM_CHANNEL_GET, false); - } - }); - } - - set_test(THREAD_CREATE, false); - if (!offline) { - bot.thread_create("thread test", TEST_TEXT_CHANNEL_ID, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 60, [&](const dpp::confirmation_callback_t &event) { - if (!event.is_error()) { - [[maybe_unused]] const auto &thread = event.get(); - set_test(THREAD_CREATE, true); - } - // the thread tests are in the on_thread_create event handler - }); - } - - set_test(MEMBER_GET, false); - if (!offline) { - bot.guild_get_member(TEST_GUILD_ID, TEST_USER_ID, [](const dpp::confirmation_callback_t &event){ - if (!event.is_error()) { - dpp::guild_member m = std::get(event.value); - if (m.guild_id == TEST_GUILD_ID && m.user_id == TEST_USER_ID) { - set_test(MEMBER_GET, true); - } else { - set_test(MEMBER_GET, false); - } - } else { - set_test(MEMBER_GET, false); - } - }); - } - - set_test(ROLE_CREATE, false); - set_test(ROLE_EDIT, false); - set_test(ROLE_DELETE, false); - if (!offline) { - dpp::role r; - r.guild_id = TEST_GUILD_ID; - r.name = "Test-Role"; - r.permissions.add(dpp::p_move_members); - r.set_flags(dpp::r_mentionable); - r.colour = dpp::colors::moon_yellow; - dpp::role createdRole; - try { - createdRole = bot.role_create_sync(r); - if (createdRole.name == r.name && - createdRole.has_move_members() && - createdRole.flags & dpp::r_mentionable && - createdRole.colour == r.colour) { - set_test(ROLE_CREATE, true); - } - } catch (dpp::rest_exception &exception) { - set_test(ROLE_CREATE, false); - } - createdRole.guild_id = TEST_GUILD_ID; - createdRole.name = "Test-Role-Edited"; - createdRole.colour = dpp::colors::light_sea_green; - try { - dpp::role edited = bot.role_edit_sync(createdRole); - if (createdRole.id == edited.id && edited.name == "Test-Role-Edited") { - set_test(ROLE_EDIT, true); - } - } catch (dpp::rest_exception &exception) { - set_test(ROLE_EDIT, false); - } - try { - bot.role_delete_sync(TEST_GUILD_ID, createdRole.id); - set_test(ROLE_DELETE, true); - } catch (dpp::rest_exception &exception) { - set_test(ROLE_DELETE, false); - } - } - }; - - set_test(BOTSTART, false); - try { - if (!offline) { - bot.start(true); - set_test(BOTSTART, true); - } - } - catch (const std::exception &) { - set_test(BOTSTART, false); - } - - set_test(TIMERSTART, false); - uint32_t ticks = 0; - dpp::timer th = bot.start_timer([&](dpp::timer timer_handle) { - if (ticks == 5) { - /* The simple test timer ticks every second. - * If we get to 5 seconds, we know the timer is working. - */ - set_test(TIMERSTART, true); - } - ticks++; - }, 1); - - set_test(TIMEDLISTENER, false); - dpp::timed_listener tl(&bot, 10, bot.on_log, [&](const dpp::log_t & event) { - set_test(TIMEDLISTENER, true); - }); - - set_test(ONESHOT, false); - bool once = false; - dpp::oneshot_timer ost(&bot, 5, [&](dpp::timer timer_handle) { - if (!once) { - set_test(ONESHOT, true); - } else { - set_test(ONESHOT, false); - } - once = true; - }); - - // online tests - if (!offline) { - if (std::future_status status = ready_future.wait_for(std::chrono::seconds(20)); status != std::future_status::timeout) { - do_online_tests(); - } - } - - noparam_api_test(current_user_get, dpp::user_identified, CURRENTUSER); - singleparam_api_test(channel_get, TEST_TEXT_CHANNEL_ID, dpp::channel, GETCHAN); - singleparam_api_test(guild_get, TEST_GUILD_ID, dpp::guild, GETGUILD); - singleparam_api_test_list(roles_get, TEST_GUILD_ID, dpp::role_map, GETROLES); - singleparam_api_test_list(channels_get, TEST_GUILD_ID, dpp::channel_map, GETCHANS); - singleparam_api_test_list(guild_get_invites, TEST_GUILD_ID, dpp::invite_map, GETINVS); - multiparam_api_test_list(guild_get_bans, TEST_GUILD_ID, dpp::ban_map, GETBANS); - singleparam_api_test_list(channel_pins_get, TEST_TEXT_CHANNEL_ID, dpp::message_map, GETPINS); - singleparam_api_test_list(guild_events_get, TEST_GUILD_ID, dpp::scheduled_event_map, GETEVENTS); - twoparam_api_test(guild_event_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::scheduled_event, GETEVENT); - twoparam_api_test_list(guild_event_users_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::event_member_map, GETEVENTUSERS); - - std::this_thread::sleep_for(std::chrono::seconds(20)); - - /* Test stopping timer */ - set_test(TIMERSTOP, false); - set_test(TIMERSTOP, bot.stop_timer(th)); - - set_test(USERCACHE, false); - if (!offline) { - dpp::user* u = dpp::find_user(TEST_USER_ID); - set_test(USERCACHE, u); - } - set_test(CHANNELCACHE, false); - set_test(CHANNELTYPES, false); - if (!offline) { - dpp::channel* c = dpp::find_channel(TEST_TEXT_CHANNEL_ID); - dpp::channel* c2 = dpp::find_channel(TEST_VC_ID); - set_test(CHANNELCACHE, c && c2); - set_test(CHANNELTYPES, c && c->is_text_channel() && !c->is_voice_channel() && c2 && c2->is_voice_channel() && !c2->is_text_channel()); - } - - wait_for_tests(); - - } - catch (const std::exception &e) { - std::cout << e.what() << "\n"; - set_test(CLUSTER, false); - } -} diff --git a/src/unittest/http.cpp b/src/unittest/http.cpp deleted file mode 100644 index 6f8639b86a..0000000000 --- a/src/unittest/http.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/************************************************************************************ - * - * D++, A Lightweight C++ library for Discord - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2021 Craig Edwards and D++ contributors - * (https://github.com/brainboxdotcc/DPP/graphs/contributors) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ************************************************************************************/ -#include "test.h" - -/* Unit tests for HTTPS client */ -void http_client_tests(const std::string& token) { - dpp::http_connect_info hci; - set_test(HOSTINFO, false); - - hci = dpp::https_client::get_host_info("https://test.com:444"); - bool hci_test = (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 444 && hci.is_ssl == true); - - hci = dpp::https_client::get_host_info("https://test.com"); - hci_test = hci_test && (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 443 && hci.is_ssl == true); - - hci = dpp::https_client::get_host_info("http://test.com"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); - - hci = dpp::https_client::get_host_info("http://test.com:90"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 90 && hci.is_ssl == false); - - hci = dpp::https_client::get_host_info("test.com:97"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 97 && hci.is_ssl == false); - - hci = dpp::https_client::get_host_info("test.com"); - hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); - - set_test(HOSTINFO, hci_test); - - set_test(HTTPS, false); - if (!offline) { - dpp::multipart_content multipart = dpp::https_client::build_multipart( - "{\"content\":\"test\"}", {"test.txt", "blob.blob"}, {"ABCDEFGHI", "BLOB!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"}, {"text/plain", "application/octet-stream"} - ); - try { - dpp::https_client c("discord.com", 443, "/api/channels/" + std::to_string(TEST_TEXT_CHANNEL_ID) + "/messages", "POST", multipart.body, - { - {"Content-Type", multipart.mimetype}, - {"Authorization", "Bot " + token} - } - ); - std::string hdr1 = c.get_header("server"); - set_test(HTTPS, hdr1 == "cloudflare" && c.get_status() == 200); - } - catch (const dpp::exception& e) { - std::cout << e.what() << "\n"; - set_test(HTTPS, false); - } - } - - set_test(HTTP, false); - try { - dpp::https_client c2("github.com", 80, "/", "GET", "", {}, true); - std::string hdr2 = c2.get_header("location"); - set_test(HTTP, hdr2 == "https://github.com/" && c2.get_status() == 301); - } - catch (const dpp::exception& e) { - std::cout << e.what() << "\n"; - set_test(HTTP, false); - } - - set_test(MULTIHEADER, false); - try { - dpp::https_client c2("www.google.com", 80, "/", "GET", "", {}, true); - size_t count = c2.get_header_count("set-cookie"); - size_t count_list = c2.get_header_list("set-cookie").size(); - // Google sets a bunch of cookies when we start accessing it. - set_test(MULTIHEADER, c2.get_status() == 200 && count > 1 && count == count_list); - } - catch (const dpp::exception& e) { - std::cout << e.what() << "\n"; - set_test(MULTIHEADER, false); - } -} diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 8270d0687b..086e0336f9 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -21,6 +21,49 @@ ************************************************************************************/ #include "test.h" +#include +#include +#include +#include + +/** + * @brief Type trait to check if a certain type has a build_json method + * + * @tparam T type to check for + */ +template > +struct has_build_json : std::false_type {}; + +template +struct has_build_json().build_json())>> : std::true_type {}; + +/** + * @brief Type trait to check if a certain type has a build_json method + * + * @tparam T type to check for + */ +template +constexpr bool has_build_json_v = has_build_json::value; + +/** + * @brief Type trait to check if a certain type has a fill_from_json method + * + * @tparam T type to check for + */ +template +struct has_fill_from_json : std::false_type {}; + +template +struct has_fill_from_json().fill_from_json(std::declval()))>> : std::true_type {}; + +/** + * @brief Type trait to check if a certain type has a fill_from_json method + * + * @tparam T type to check for + */ +template +constexpr bool has_fill_from_json_v = has_fill_from_json::value; + /* Unit tests go here */ int main(int argc, char *argv[]) { @@ -36,19 +79,2161 @@ int main(int argc, char *argv[]) std::cout << "Running offline and " << (extended ? "extended" : "limited") << " online unit tests. Guild ID: " << TEST_GUILD_ID << " Text Channel ID: " << TEST_TEXT_CHANNEL_ID << " VC ID: " << TEST_VC_ID << " User ID: " << TEST_USER_ID << " Event ID: " << TEST_EVENT_ID << "\n"; } - dpp::cluster bot(token, dpp::i_all_intents); + std::string test_to_escape = "*** _This is a test_ ***\n```cpp\n\ +int main() {\n\ + /* Comment */\n\ + int answer = 42;\n\ + return answer; // ___\n\ +};\n\ +```\n\ +Markdown lol ||spoiler|| ~~strikethrough~~ `small *code* block`\n"; + + set_test(COMPARISON, false); + dpp::user u1; + dpp::user u2; + dpp::user u3; + u1.id = u2.id = 666; + u3.id = 777; + set_test(COMPARISON, u1 == u2 && u1 != u3); + + + set_test(ERRORS, false); + + /* Prepare a confirmation_callback_t in error state (400) */ + dpp::confirmation_callback_t error_test; + bool error_message_success = false; + error_test.http_info.status = 400; + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"options\": {\ + \"0\": {\ + \"name\": {\ + \"_errors\": [\ + {\ + \"code\": \"STRING_TYPE_REGEX\",\ + \"message\": \"String value did not match validation regex.\"\ + },\ + {\ + \"code\": \"APPLICATION_COMMAND_INVALID_NAME\",\ + \"message\": \"Command name is invalid\"\ + }\ + ]\ + }\ + }\ + }\ + }\ + }"; + error_message_success = (error_test.get_error().human_readable == "50035: Invalid Form Body\n\t- options[0].name: String value did not match validation regex. (STRING_TYPE_REGEX)\n\t- options[0].name: Command name is invalid (APPLICATION_COMMAND_INVALID_NAME)"); + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"type\": {\ + \"_errors\": [\ + {\ + \"code\": \"BASE_TYPE_CHOICES\",\ + \"message\": \"Value must be one of {4, 5, 9, 10, 11}.\"\ + }\ + ]\ + }\ + }\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - type: Value must be one of {4, 5, 9, 10, 11}. (BASE_TYPE_CHOICES)"); + + error_test.http_info.body = "{\ + \"message\": \"Unknown Guild\",\ + \"code\": 10004\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "10004: Unknown Guild"); + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"allowed_mentions\": {\ + \"_errors\": [\ + {\ + \"code\": \"MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE\",\ + \"message\": \"parse:[\\\"users\\\"] and users: [ids...] are mutually exclusive.\"\ + }\ + ]\ + }\ + }\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - allowed_mentions: parse:[\"users\"] and users: [ids...] are mutually exclusive. (MESSAGE_ALLOWED_MENTIONS_PARSE_EXCLUSIVE)"); + + error_test.http_info.body = "{\ + \"message\": \"Invalid Form Body\",\ + \"code\": 50035,\ + \"errors\": {\ + \"1\": {\ + \"options\": {\ + \"1\": {\ + \"description\": {\ + \"_errors\": [\ + {\ + \"code\": \"BASE_TYPE_BAD_LENGTH\",\ + \"message\": \"Must be between 1 and 100 in length.\"\ + }\ + ]\ + }\ + }\ + }\ + }\ + }\ + }"; + error_message_success = (error_message_success && error_test.get_error().human_readable == "50035: Invalid Form Body - [1].options[1].description: Must be between 1 and 100 in length. (BASE_TYPE_BAD_LENGTH)"); + + set_test(ERRORS, error_message_success); + + set_test(MD_ESC_1, false); + set_test(MD_ESC_2, false); + std::string escaped1 = dpp::utility::markdown_escape(test_to_escape); + std::string escaped2 = dpp::utility::markdown_escape(test_to_escape, true); + set_test(MD_ESC_1, escaped1 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ +```cpp\n\ +int main() {\n\ + /* Comment */\n\ + int answer = 42;\n\ + return answer; // ___\n\ +};\n\ +```\n\ +Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ `small *code* block`\n"); + set_test(MD_ESC_2, escaped2 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ +\\`\\`\\`cpp\n\ +int main\\(\\) {\n\ + /\\* Comment \\*/\n\ + int answer = 42;\n\ + return answer; // \\_\\_\\_\n\ +};\n\ +\\`\\`\\`\n\ +Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* block\\`\n"); + + set_test(URLENC, false); + set_test(URLENC, dpp::utility::url_encode("ABC123_+\\|$*/AAA[]😄") == "ABC123_%2B%5C%7C%24%2A%2FAAA%5B%5D%F0%9F%98%84"); + + set_test(BASE64ENC, false); + set_test(BASE64ENC, + dpp::base64_encode(reinterpret_cast("a"), 1) == "YQ==" && + dpp::base64_encode(reinterpret_cast("bc"), 2) == "YmM=" && + dpp::base64_encode(reinterpret_cast("def"), 3) == "ZGVm" && + dpp::base64_encode(reinterpret_cast("ghij"), 4) == "Z2hpag==" && + dpp::base64_encode(reinterpret_cast("klmno"), 5) == "a2xtbm8=" && + dpp::base64_encode(reinterpret_cast("pqrstu"), 6) == "cHFyc3R1" && + dpp::base64_encode(reinterpret_cast("vwxyz12"), 7) == "dnd4eXoxMg==" + ); + + dpp::http_connect_info hci; + set_test(HOSTINFO, false); + + hci = dpp::https_client::get_host_info("https://test.com:444"); + bool hci_test = (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 444 && hci.is_ssl == true); + + hci = dpp::https_client::get_host_info("https://test.com"); + hci_test = hci_test && (hci.scheme == "https" && hci.hostname == "test.com" && hci.port == 443 && hci.is_ssl == true); + + hci = dpp::https_client::get_host_info("http://test.com"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); + + hci = dpp::https_client::get_host_info("http://test.com:90"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 90 && hci.is_ssl == false); + + hci = dpp::https_client::get_host_info("test.com:97"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 97 && hci.is_ssl == false); + + hci = dpp::https_client::get_host_info("test.com"); + hci_test = hci_test && (hci.scheme == "http" && hci.hostname == "test.com" && hci.port == 80 && hci.is_ssl == false); + + set_test(HOSTINFO, hci_test); + + set_test(HTTPS, false); + if (!offline) { + dpp::multipart_content multipart = dpp::https_client::build_multipart( + "{\"content\":\"test\"}", {"test.txt", "blob.blob"}, {"ABCDEFGHI", "BLOB!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!"}, {"text/plain", "application/octet-stream"} + ); + try { + dpp::https_client c("discord.com", 443, "/api/channels/" + std::to_string(TEST_TEXT_CHANNEL_ID) + "/messages", "POST", multipart.body, + { + {"Content-Type", multipart.mimetype}, + {"Authorization", "Bot " + token} + } + ); + std::string hdr1 = c.get_header("server"); + std::string content1 = c.get_content(); + set_test(HTTPS, hdr1 == "cloudflare" && c.get_status() == 200); + } + catch (const dpp::exception& e) { + std::cout << e.what() << "\n"; + set_test(HTTPS, false); + } + } + + set_test(HTTP, false); + try { + dpp::https_client c2("github.com", 80, "/", "GET", "", {}, true); + std::string hdr2 = c2.get_header("location"); + std::string content2 = c2.get_content(); + set_test(HTTP, hdr2 == "https://github.com/" && c2.get_status() == 301); + } + catch (const dpp::exception& e) { + std::cout << e.what() << "\n"; + set_test(HTTP, false); + } + + set_test(MULTIHEADER, false); + try { + dpp::https_client c2("www.google.com", 80, "/", "GET", "", {}, true); + size_t count = c2.get_header_count("set-cookie"); + size_t count_list = c2.get_header_list("set-cookie").size(); + // Google sets a bunch of cookies when we start accessing it. + set_test(MULTIHEADER, c2.get_status() == 200 && count > 1 && count == count_list); + } + catch (const dpp::exception& e) { + std::cout << e.what() << "\n"; + set_test(MULTIHEADER, false); + } + + std::vector testaudio = load_test_audio(); + + set_test(READFILE, false); + std::string rf_test = dpp::utility::read_file(SHARED_OBJECT); + FILE* fp = fopen(SHARED_OBJECT, "rb"); + fseek(fp, 0, SEEK_END); + size_t off = (size_t)ftell(fp); + fclose(fp); + set_test(READFILE, off == rf_test.length()); + + set_test(TIMESTAMPTOSTRING, false); + set_test(TIMESTAMPTOSTRING, dpp::ts_to_string(1642611864) == "2022-01-19T17:04:24Z"); + + set_test(ROLE_COMPARE, false); + dpp::role role_1, role_2; + role_1.position = 1; + role_2.position = 2; + set_test(ROLE_COMPARE, role_1 < role_2 && role_1 != role_2); + + set_test(WEBHOOK, false); + try { + dpp::webhook test_wh("https://discord.com/api/webhooks/833047646548133537/ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE"); + set_test(WEBHOOK, (test_wh.token == "ntCHEYYIoHSLy_GOxPx6pmM0sUoLbP101ct-WI6F-S4beAV2vaIcl_Id5loAMyQwxqhE") && (test_wh.id == dpp::snowflake(833047646548133537))); + } + catch (const dpp::exception&) { + set_test(WEBHOOK, false); + } + + { // test dpp::snowflake + start_test(SNOWFLAKE); + bool success = true; + dpp::snowflake s = 69420; + json j; + j["value"] = s; + success = dpp::snowflake_not_null(&j, "value") == 69420 && success; + DPP_CHECK_CONSTRUCT_ASSIGN(SNOWFLAKE, dpp::snowflake, success); + s = 42069; + success = success && (s == 42069 && s == dpp::snowflake{42069} && s == "42069"); + success = success && (dpp::snowflake{69} < dpp::snowflake{420} && (dpp::snowflake{69} < 420)); + s = "69420"; + success = success && s == 69420; + auto conversion_test = [](dpp::snowflake sl) { + return sl.str(); + }; + s = conversion_test(std::string{"1337"}); + success = success && s == 1337; /* THIS BREAKS (and i do not care very much): && s == conversion_test(dpp::snowflake{"1337"}); */ + success = success && dpp::snowflake{0} == 0; + set_test(SNOWFLAKE, success); + } + + { // test dpp::json_interface + start_test(JSON_INTERFACE); + struct fillable : dpp::json_interface { + fillable &fill_from_json_impl(dpp::json *) { + return *this; + } + }; + struct buildable : dpp::json_interface { + json to_json_impl(bool = false) const { + return {}; + } + }; + struct fillable_and_buildable : dpp::json_interface { + fillable_and_buildable &fill_from_json_impl(dpp::json *) { + return *this; + } + + json to_json_impl(bool = false) const { + return {}; + } + }; + bool success = true; + + DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); + DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); + DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v, success); + + DPP_CHECK(JSON_INTERFACE, !has_build_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); + DPP_CHECK(JSON_INTERFACE, !has_build_json_v, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); + + DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); + DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); + DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); + set_test(JSON_INTERFACE, success); + } + + { // test interaction_create_t::get_parameter + // create a fake interaction + dpp::cluster cluster(""); + dpp::discord_client client(&cluster, 1, 1, ""); + dpp::interaction_create_t interaction(&client, ""); + + /* Check the method with subcommands */ + set_test(GET_PARAMETER_WITH_SUBCOMMANDS, false); + + dpp::command_interaction cmd_data; // command + cmd_data.type = dpp::ctxm_chat_input; + cmd_data.name = "command"; + + dpp::command_data_option subcommandgroup; // subcommand group + subcommandgroup.name = "group"; + subcommandgroup.type = dpp::co_sub_command_group; + + dpp::command_data_option subcommand; // subcommand + subcommand.name = "add"; + subcommand.type = dpp::co_sub_command; + + dpp::command_data_option option1; // slashcommand option + option1.name = "user"; + option1.type = dpp::co_user; + option1.value = dpp::snowflake(189759562910400512); + + dpp::command_data_option option2; // slashcommand option + option2.name = "checked"; + option2.type = dpp::co_boolean; + option2.value = true; + + // add them + subcommand.options.push_back(option1); + subcommand.options.push_back(option2); + subcommandgroup.options.push_back(subcommand); + cmd_data.options.push_back(subcommandgroup); + interaction.command.data = cmd_data; + + dpp::snowflake value1 = std::get(interaction.get_parameter("user")); + set_test(GET_PARAMETER_WITH_SUBCOMMANDS, value1 == dpp::snowflake(189759562910400512)); + + /* Check the method without subcommands */ + set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, false); + + dpp::command_interaction cmd_data2; // command + cmd_data2.type = dpp::ctxm_chat_input; + cmd_data2.name = "command"; + + dpp::command_data_option option3; // slashcommand option + option3.name = "number"; + option3.type = dpp::co_integer; + option3.value = int64_t(123456); + + cmd_data2.options.push_back(option3); + interaction.command.data = cmd_data2; + + int64_t value2 = std::get(interaction.get_parameter("number")); + set_test(GET_PARAMETER_WITHOUT_SUBCOMMANDS, value2 == 123456); + } + + { // test dpp::command_option_choice::fill_from_json + set_test(OPTCHOICE_DOUBLE, false); + set_test(OPTCHOICE_INT, false); + set_test(OPTCHOICE_BOOL, false); + set_test(OPTCHOICE_SNOWFLAKE, false); + set_test(OPTCHOICE_STRING, false); + json j; + dpp::command_option_choice choice; + j["value"] = 54.321; + choice.fill_from_json(&j); + bool success_double = std::holds_alternative(choice.value); + j["value"] = 8223372036854775807; + choice.fill_from_json(&j); + bool success_int = std::holds_alternative(choice.value); + j["value"] = -8223372036854775807; + choice.fill_from_json(&j); + bool success_int2 = std::holds_alternative(choice.value); + j["value"] = true; + choice.fill_from_json(&j); + bool success_bool = std::holds_alternative(choice.value); + dpp::snowflake s(845266178036516757); // example snowflake + j["value"] = s; + choice.fill_from_json(&j); + bool success_snowflake = std::holds_alternative(choice.value) && std::get(choice.value) == s; + j["value"] = "foobar"; + choice.fill_from_json(&j); + bool success_string = std::holds_alternative(choice.value); + set_test(OPTCHOICE_DOUBLE, success_double); + set_test(OPTCHOICE_INT, success_int && success_int2); + set_test(OPTCHOICE_BOOL, success_bool); + set_test(OPTCHOICE_SNOWFLAKE, success_snowflake); + set_test(OPTCHOICE_STRING, success_string); + } + + { + set_test(PERMISSION_CLASS, false); + bool success = false; + auto p = dpp::permission(); + p = 16; + success = p == 16; + p |= 4; + success = p == 20 && success; + p <<= 8; // left shift + success = p == 5120 && success; + auto s = std::to_string(p); + success = s == "5120" && success; + p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_administrator); + success = !p.has(dpp::p_administrator, dpp::p_ban_members) && success; // must return false because they're not both set + success = !p.has(dpp::p_administrator | dpp::p_ban_members) && success; + success = p.can(dpp::p_ban_members) && success; + success = p.can(dpp::p_speak) && success; + + constexpr auto permission_test = [](dpp::permission p) constexpr noexcept { + bool success{true}; + + p.set(0).add(~uint64_t{0}).remove(dpp::p_speak).set(dpp::p_connect); + p.set(dpp::p_administrator, dpp::p_ban_members); + success = p.has(dpp::p_administrator) && success; + success = p.has(dpp::p_administrator) && p.has(dpp::p_ban_members) && success; + success = p.has(dpp::p_administrator, dpp::p_ban_members) && success; + success = p.has(dpp::p_administrator | dpp::p_ban_members) && success; + success = p.add(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; + success = !p.remove(dpp::p_speak).has(dpp::p_administrator, dpp::p_speak) && success; + p.remove(dpp::p_administrator); + success = p.can(dpp::p_ban_members) && success; + success = !p.can(dpp::p_speak, dpp::p_ban_members) && success; + success = p.can_any(dpp::p_speak, dpp::p_ban_members) && success; + return success; + }; + constexpr auto constexpr_success = permission_test({~uint64_t{0}}); // test in constant evaluated + success = permission_test({~uint64_t{0}}) && constexpr_success && success; // test at runtime + set_test(PERMISSION_CLASS, success); + } + + { // dpp event classes + start_test(EVENT_CLASS); + bool success = true; + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::log_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_user_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_scheduled_event_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::automod_rule_execute_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::stage_instance_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_state_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::interaction_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::slashcommand_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::button_click_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::form_submit_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::autocomplete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::context_menu_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_context_menu_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_context_menu_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::select_click_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_stickers_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_join_request_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::ready_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::resumed_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::typing_start_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_track_marker_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_members_chunk_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_emoji_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_delete_bulk_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_role_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::channel_pins_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_reaction_remove_all_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_server_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_emojis_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::presence_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::webhooks_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_integrations_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_member_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::invite_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::user_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::message_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_audit_log_entry_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_add_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::guild_ban_remove_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::integration_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_create_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_delete_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_list_sync_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_member_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::thread_members_update_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_buffer_send_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_user_talking_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_ready_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_receive_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_speaking_t, success); + DPP_CHECK_CONSTRUCT_ASSIGN(EVENT_CLASS, dpp::voice_client_disconnect_t, success); + set_test(EVENT_CLASS, success); + } + + + { // some dpp::user methods + dpp::user user1; + user1.id = 189759562910400512; + user1.discriminator = 0001; + user1.username = "brain"; + + set_test(USER_GET_MENTION, false); + set_test(USER_GET_MENTION, user1.get_mention() == "<@189759562910400512>"); + + set_test(USER_FORMAT_USERNAME, false); + set_test(USER_FORMAT_USERNAME, user1.format_username() == "brain#0001"); + + set_test(USER_GET_CREATION_TIME, false); + set_test(USER_GET_CREATION_TIME, (uint64_t)user1.get_creation_time() == 1465312605); + + set_test(USER_GET_URL, false); + + dpp::user user2; + set_test(USER_GET_URL, + user1.get_url() == dpp::utility::url_host + "/users/189759562910400512" && + user2.get_url() == "" + ); + } + + { // avatar size function + set_test(UTILITY_AVATAR_SIZE, false); + bool success = false; + success = dpp::utility::avatar_size(0).empty(); + success = dpp::utility::avatar_size(16) == "?size=16" && success; + success = dpp::utility::avatar_size(256) == "?size=256" && success; + success = dpp::utility::avatar_size(4096) == "?size=4096" && success; + success = dpp::utility::avatar_size(8192).empty() && success; + success = dpp::utility::avatar_size(3000).empty() && success; + set_test(UTILITY_AVATAR_SIZE, success); + } + + { // cdn endpoint url getter + set_test(UTILITY_CDN_ENDPOINT_URL_HASH, false); + bool success = false; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_jpg, 0).empty(); + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 0) == "https://cdn.discordapp.com/foobar/test.png" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png }, "foobar/test", "", dpp::i_png, 128) == "https://cdn.discordapp.com/foobar/test.png?size=128" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_gif, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, true) == "https://cdn.discordapp.com/foobar/test/a_12345.png" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, false, false) == "https://cdn.discordapp.com/foobar/test/12345.png" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "12345", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test/a_12345.gif" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_png, 0, true, true) == "https://cdn.discordapp.com/foobar/test.gif" && success; + success = dpp::utility::cdn_endpoint_url_hash({ dpp::i_png, dpp::i_gif }, "foobar/test", "", dpp::i_gif, 0, false, false).empty() && success; + set_test(UTILITY_CDN_ENDPOINT_URL_HASH, success); + } + + { // sticker url getter + set_test(STICKER_GET_URL, false); + dpp::sticker s; + s.format_type = dpp::sf_png; + bool success = s.get_url().empty(); + s.id = 12345; + success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.png" && success; + s.format_type = dpp::sf_gif; + success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.gif" && success; + s.format_type = dpp::sf_lottie; + success = s.get_url() == "https://cdn.discordapp.com/stickers/12345.json" && success; + set_test(STICKER_GET_URL, success); + } + + { // user url getter + dpp::user user1; + user1.id = 189759562910400512; + user1.username = "Brain"; + user1.discriminator = 0001; + + auto user2 = user1; + user2.avatar = "5532c6414c70765a28cf9448c117205f"; + + auto user3 = user2; + user3.flags |= dpp::u_animated_icon; + + set_test(USER_GET_AVATAR_URL, false); + set_test(USER_GET_AVATAR_URL, + dpp::user().get_avatar_url().empty() && + user1.get_avatar_url() == dpp::utility::cdn_host + "/embed/avatars/1.png" && + user2.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.png" && + user2.get_avatar_url(0, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.webp" && + user2.get_avatar_url(0, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/5532c6414c70765a28cf9448c117205f.jpg" && + user3.get_avatar_url() == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" && + user3.get_avatar_url(4096, dpp::i_gif) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=4096" && + user3.get_avatar_url(512, dpp::i_webp) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && + user3.get_avatar_url(512, dpp::i_jpg) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif?size=512" && + user3.get_avatar_url(16, dpp::i_jpg, false) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.jpg?size=16" && + user3.get_avatar_url(5000) == dpp::utility::cdn_host + "/avatars/189759562910400512/a_5532c6414c70765a28cf9448c117205f.gif" + ); + } + + { // emoji url getter + dpp::emoji emoji; + emoji.id = 825407338755653641; + + set_test(EMOJI_GET_URL, false); + set_test(EMOJI_GET_URL, emoji.get_url() == dpp::utility::cdn_host + "/emojis/825407338755653641.png"); + } + + { // message methods + dpp::message m; + m.guild_id = 825407338755653642; + m.channel_id = 956230231277072415; + m.id = 1151617986541666386; + + dpp::message m2; + m2.guild_id = 825407338755653642; + m2.channel_id = 956230231277072415; + + dpp::message m3; + m3.guild_id = 825407338755653642; + m3.id = 1151617986541666386; + + dpp::message m4; + m4.channel_id = 956230231277072415; + m4.id = 1151617986541666386; + + dpp::message m5; + m5.guild_id = 825407338755653642; + + dpp::message m6; + m6.channel_id = 956230231277072415; + + dpp::message m7; + m7.id = 1151617986541666386; + + dpp::message m8; + + set_test(MESSAGE_GET_URL, false); + set_test(MESSAGE_GET_URL, + m.get_url() == dpp::utility::url_host + "/channels/825407338755653642/956230231277072415/1151617986541666386" && + m2.get_url() == "" && + m3.get_url() == "" && + m4.get_url() == "" && + m5.get_url() == "" && + m6.get_url() == "" && + m7.get_url() == "" && + m8.get_url() == "" + ); + } + + { // channel methods + set_test(CHANNEL_SET_TYPE, false); + dpp::channel c; + c.set_flags(dpp::c_nsfw | dpp::c_video_quality_720p); + c.set_type(dpp::CHANNEL_CATEGORY); + bool before = c.is_category() && !c.is_forum(); + c.set_type(dpp::CHANNEL_FORUM); + bool after = !c.is_category() && c.is_forum(); + set_test(CHANNEL_SET_TYPE, before && after); + + set_test(CHANNEL_GET_MENTION, false); + c.id = 825411707521728511; + set_test(CHANNEL_GET_MENTION, c.get_mention() == "<#825411707521728511>"); + + set_test(CHANNEL_GET_URL, false); + c.guild_id = 825407338755653642; + + dpp::channel c2; + c2.id = 825411707521728511; + + dpp::channel c3; + c3.guild_id = 825407338755653642; + + dpp::channel c4; + + set_test(CHANNEL_GET_URL, + c.get_url() == dpp::utility::url_host + "/channels/825407338755653642/825411707521728511" && + c2.get_url() == "" && + c3.get_url() == "" && + c4.get_url() == "" + ); + } + + { // utility methods + set_test(UTILITY_GUILD_NAVIGATION, false); + auto gn1 = dpp::utility::guild_navigation(123, dpp::utility::gnt_customize); + auto gn2 = dpp::utility::guild_navigation(1234, dpp::utility::gnt_browse); + auto gn3 = dpp::utility::guild_navigation(12345, dpp::utility::gnt_guide); + set_test(UTILITY_GUILD_NAVIGATION, gn1 == "<123:customize>" && gn2 == "<1234:browse>" && gn3 == "<12345:guide>"); + + set_test(UTILITY_ICONHASH, false); + auto iconhash1 = dpp::utility::iconhash("a_5532c6414c70765a28cf9448c117205f"); + set_test(UTILITY_ICONHASH, iconhash1.first == 6139187225817019994 && + iconhash1.second == 2940732121894297695 && + iconhash1.to_string() == "5532c6414c70765a28cf9448c117205f" + ); + + set_test(UTILITY_MAKE_URL_PARAMETERS, false); + auto url_params1 = dpp::utility::make_url_parameters({ + {"foo", 15}, + {"bar", 7} + }); + auto url_params2 = dpp::utility::make_url_parameters({ + {"foo", "hello"}, + {"bar", "two words"} + }); + set_test(UTILITY_MAKE_URL_PARAMETERS, url_params1 == "?bar=7&foo=15" && url_params2 == "?bar=two%20words&foo=hello"); + + set_test(UTILITY_MARKDOWN_ESCAPE, false); + auto escaped = dpp::utility::markdown_escape( + "> this is a quote\n" + "**some bold text**"); + set_test(UTILITY_MARKDOWN_ESCAPE, "\\>this is a quote\\n\\*\\*some bold text\\*\\*"); + + set_test(UTILITY_TOKENIZE, false); + auto tokens = dpp::utility::tokenize("some Whitespace seperated Text to Tokenize", " "); + std::vector expected_tokens = {"some", "Whitespace", "seperated", "Text", "to", "Tokenize"}; + set_test(UTILITY_TOKENIZE, tokens == expected_tokens); + + set_test(UTILITY_URL_ENCODE, false); + auto url_encoded = dpp::utility::url_encode("S2-^$1Nd+U!g'8+_??o?p-bla bla"); + set_test(UTILITY_URL_ENCODE, url_encoded == "S2-%5E%241Nd%2BU%21g%278%2B_%3F%3Fo%3Fp-bla%20bla"); + + set_test(UTILITY_SLASHCOMMAND_MENTION, false); + auto mention1 = dpp::utility::slashcommand_mention(123, "name"); + auto mention2 = dpp::utility::slashcommand_mention(123, "name", "sub"); + auto mention3 = dpp::utility::slashcommand_mention(123, "name", "group", "sub"); + bool success = mention1 == "" && mention2 == "" && mention3 == ""; + set_test(UTILITY_SLASHCOMMAND_MENTION, success); + + set_test(UTILITY_CHANNEL_MENTION, false); + auto channel_mention = dpp::utility::channel_mention(123); + set_test(UTILITY_CHANNEL_MENTION, channel_mention == "<#123>"); + + set_test(UTILITY_USER_MENTION, false); + auto user_mention = dpp::utility::user_mention(123); + set_test(UTILITY_USER_MENTION, user_mention == "<@123>"); + + set_test(UTILITY_ROLE_MENTION, false); + auto role_mention = dpp::utility::role_mention(123); + set_test(UTILITY_ROLE_MENTION, role_mention == "<@&123>"); + + set_test(UTILITY_EMOJI_MENTION, false); + auto emoji_mention1 = dpp::utility::emoji_mention("role1", 123, false); + auto emoji_mention2 = dpp::utility::emoji_mention("role2", 234, true); + auto emoji_mention3 = dpp::utility::emoji_mention("white_check_mark", 0, false); + auto emoji_mention4 = dpp::utility::emoji_mention("white_check_mark", 0, true); + set_test(UTILITY_EMOJI_MENTION, + emoji_mention1 == "<:role1:123>" && + emoji_mention2 == "" && + emoji_mention3 == ":white_check_mark:" && + emoji_mention4 == ":white_check_mark:" + ); + + set_test(UTILITY_USER_URL, false); + auto user_url = dpp::utility::user_url(123); + set_test(UTILITY_USER_URL, + user_url == dpp::utility::url_host + "/users/123" && + dpp::utility::user_url(0) == "" + ); + + set_test(UTILITY_MESSAGE_URL, false); + auto message_url = dpp::utility::message_url(1,2,3); + set_test(UTILITY_MESSAGE_URL, + message_url == dpp::utility::url_host+ "/channels/1/2/3" && + dpp::utility::message_url(0,2,3) == "" && + dpp::utility::message_url(1,0,3) == "" && + dpp::utility::message_url(1,2,0) == "" && + dpp::utility::message_url(0,0,3) == "" && + dpp::utility::message_url(0,2,0) == "" && + dpp::utility::message_url(1,0,0) == "" && + dpp::utility::message_url(0,0,0) == "" + ); + + set_test(UTILITY_CHANNEL_URL, false); + auto channel_url = dpp::utility::channel_url(1,2); + set_test(UTILITY_CHANNEL_URL, + channel_url == dpp::utility::url_host+ "/channels/1/2" && + dpp::utility::channel_url(0,2) == "" && + dpp::utility::channel_url(1,0) == "" && + dpp::utility::channel_url(0,0) == "" + ); + + set_test(UTILITY_THREAD_URL, false); + auto thread_url = dpp::utility::thread_url(1,2); + set_test(UTILITY_THREAD_URL, + thread_url == dpp::utility::url_host+ "/channels/1/2" && + dpp::utility::thread_url(0,2) == "" && + dpp::utility::thread_url(1,0) == "" && + dpp::utility::thread_url(0,0) == "" + ); + } + +#ifndef _WIN32 + set_test(TIMESTRINGTOTIMESTAMP, false); + json tj; + tj["t1"] = "2022-01-19T17:18:14.506000+00:00"; + tj["t2"] = "2022-01-19T17:18:14+00:00"; + uint32_t inTimestamp = 1642612694; + set_test(TIMESTRINGTOTIMESTAMP, (uint64_t)dpp::ts_not_null(&tj, "t1") == inTimestamp && (uint64_t)dpp::ts_not_null(&tj, "t2") == inTimestamp); +#else + set_test(TIMESTRINGTOTIMESTAMP, true); +#endif - errors_test(); - http_client_tests(token); - discord_objects_tests(); - gateway_events_tests(token, bot); - cache_tests(bot); - utility_tests(); + { + set_test(TS, false); + dpp::managed m(189759562910400512); + set_test(TS, ((uint64_t) m.get_creation_time()) == 1465312605); + } { coro_offline_tests(); } + std::vector test_image = load_test_image(); + + set_test(PRESENCE, false); + set_test(CLUSTER, false); + try { + dpp::cluster bot(token, dpp::i_all_intents); + bot.set_websocket_protocol(dpp::ws_etf); + set_test(CLUSTER, true); + set_test(CONNECTION, false); + set_test(GUILDCREATE, false); + set_test(ICONHASH, false); + + set_test(MSGCOLLECT, false); + if (!offline) { + /* Intentional leak: freed on unit test end */ + [[maybe_unused]] + message_collector* collect_messages = new message_collector(&bot, 25); + } + + set_test(JSON_PARSE_ERROR, false); + dpp::rest_request(&bot, "/nonexistent", "address", "", dpp::m_get, "", [](const dpp::confirmation_callback_t& e) { + if (e.is_error() && e.get_error().code == 404) { + set_test(JSON_PARSE_ERROR, true); + } else { + set_test(JSON_PARSE_ERROR, false); + } + }); + + dpp::utility::iconhash i; + std::string dummyval("fcffffffffffff55acaaaaaaaaaaaa66"); + i = dummyval; + set_test(ICONHASH, (i.to_string() == dummyval)); + + /* This ensures we test both protocols, as voice is json and shard is etf */ + bot.set_websocket_protocol(dpp::ws_etf); + + bot.on_form_submit([&](const dpp::form_submit_t & event) { + }); + + /* This is near impossible to test without a 'clean room' voice channel. + * We attach this event just so that the decoder events are fired while we + * are sending audio later, this way if the audio receive code is plain unstable + * the test suite will crash and fail. + */ + bot.on_voice_receive_combined([&](const auto& event) { + }); + + std::promise ready_promise; + std::future ready_future = ready_promise.get_future(); + bot.on_ready([&](const dpp::ready_t & event) { + set_test(CONNECTION, true); + ready_promise.set_value(); + + set_test(APPCOMMAND, false); + set_test(LOGGER, false); + bot.log(dpp::ll_info, "Test log message"); + + bot.guild_command_create(dpp::slashcommand().set_name("testcommand") + .set_description("Test command for DPP unit test") + .add_option(dpp::command_option(dpp::co_attachment, "file", "a file")) + .set_application_id(bot.me.id) + .add_localization("fr", "zut", "Ou est la salor dans Discord?"), + TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(APPCOMMAND, true); + set_test(DELCOMMAND, false); + dpp::slashcommand s = std::get(callback.value); + bot.guild_command_delete(s.id, TEST_GUILD_ID, [&](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + dpp::message test_message(TEST_TEXT_CHANNEL_ID, "test message"); + + set_test(DELCOMMAND, true); + set_test(MESSAGECREATE, false); + set_test(MESSAGEEDIT, false); + set_test(MESSAGERECEIVE, false); + test_message.add_file("no-mime", "test"); + test_message.add_file("test.txt", "test", "text/plain"); + test_message.add_file("test.png", std::string{test_image.begin(), test_image.end()}, "image/png"); + bot.message_create(test_message, [&bot](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(MESSAGECREATE, true); + set_test(REACT, false); + dpp::message m = std::get(callback.value); + set_test(REACTEVENT, false); + bot.message_add_reaction(m.id, TEST_TEXT_CHANNEL_ID, "😄", [](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(REACT, true); + } else { + set_test(REACT, false); + } + }); + set_test(EDITEVENT, false); + bot.message_edit(dpp::message(m).set_content("test edit"), [](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(MESSAGEEDIT, true); + } + }); + } + }); + } else { + set_test(DELCOMMAND, false); + } + }); + } + }); + }); + + std::mutex loglock; + bot.on_log([&](const dpp::log_t & event) { + std::lock_guard locker(loglock); + if (event.severity > dpp::ll_trace) { + std::cout << "[" << std::fixed << std::setprecision(3) << (dpp::utility::time_f() - get_start_time()) << "]: [\u001b[36m" << dpp::utility::loglevel(event.severity) << "\u001b[0m] " << event.message << "\n"; + } + if (event.message == "Test log message") { + set_test(LOGGER, true); + } + }); + + set_test(RUNONCE, false); + uint8_t runs = 0; + for (int x = 0; x < 10; ++x) { + if (dpp::run_once()) { + runs++; + } + } + set_test(RUNONCE, (runs == 1)); + + bot.on_voice_ready([&](const dpp::voice_ready_t & event) { + set_test(VOICECONN, true); + dpp::discord_voice_client* v = event.voice_client; + set_test(VOICESEND, false); + if (v && v->is_ready()) { + v->send_audio_raw((uint16_t*)testaudio.data(), testaudio.size()); + } else { + set_test(VOICESEND, false); + } + }); + + bot.on_invite_create([](const dpp::invite_create_t &event) { + auto &inv = event.created_invite; + if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID && inv.created_at != 0 && inv.max_uses == 100) { + set_test(INVITE_CREATE_EVENT, true); + } + }); + + bot.on_invite_delete([](const dpp::invite_delete_t &event) { + auto &inv = event.deleted_invite; + if (!inv.code.empty() && inv.channel_id == TEST_TEXT_CHANNEL_ID && inv.guild_id == TEST_GUILD_ID) { + set_test(INVITE_DELETE_EVENT, true); + } + }); + + bot.on_voice_buffer_send([&](const dpp::voice_buffer_send_t & event) { + if (event.buffer_size == 0) { + set_test(VOICESEND, true); + } + }); + + set_test(SYNC, false); + if (!offline) { + dpp::message m = dpp::sync(&bot, &dpp::cluster::message_create, dpp::message(TEST_TEXT_CHANNEL_ID, "TEST")); + set_test(SYNC, m.content == "TEST"); + } + + bot.on_guild_create([&](const dpp::guild_create_t & event) { + if (event.created->id == TEST_GUILD_ID) { + set_test(GUILDCREATE, true); + if (event.presences.size() && event.presences.begin()->second.user_id > 0) { + set_test(PRESENCE, true); + } + dpp::guild* g = dpp::find_guild(TEST_GUILD_ID); + set_test(CACHE, false); + if (g) { + set_test(CACHE, true); + set_test(VOICECONN, false); + dpp::discord_client* s = bot.get_shard(0); + s->connect_voice(g->id, TEST_VC_ID, false, false); + } + else { + set_test(CACHE, false); + } + } + }); + + // this helper class contains logic for the message tests, deletes the message when all tests are done + class message_test_helper + { + private: + std::mutex mutex; + bool pin_tested; + bool thread_tested; + std::array files_tested; + std::array files_success; + dpp::snowflake channel_id; + dpp::snowflake message_id; + dpp::cluster ⊥ + + void delete_message_if_done() { + if (files_tested == std::array{true, true, true} && pin_tested && thread_tested) { + set_test(MESSAGEDELETE, false); + bot.message_delete(message_id, channel_id, [](const dpp::confirmation_callback_t &callback) { + if (!callback.is_error()) { + set_test(MESSAGEDELETE, true); + } + }); + } + } + + void set_pin_tested() { + assert(!pin_tested); + pin_tested = true; + delete_message_if_done(); + } + + void set_thread_tested() { + assert(!thread_tested); + thread_tested = true; + delete_message_if_done(); + } + + void set_file_tested(size_t index) { + assert(!files_tested[index]); + files_tested[index] = true; + if (files_tested == std::array{true, true, true}) { + set_test(MESSAGEFILE, files_success == std::array{true, true, true}); + } + delete_message_if_done(); + } + + void test_threads(const dpp::message &message) + { + set_test(THREAD_CREATE_MESSAGE, false); + set_test(THREAD_DELETE, false); + set_test(THREAD_DELETE_EVENT, false); + bot.thread_create_with_message("test", message.channel_id, message.id, 60, 60, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (callback.is_error()) { + set_thread_tested(); + } + else { + auto thread = callback.get(); + thread_id = thread.id; + set_test(THREAD_CREATE_MESSAGE, true); + bot.channel_delete(thread.id, [this](const dpp::confirmation_callback_t &callback) { + set_test(THREAD_DELETE, !callback.is_error()); + set_thread_tested(); + }); + } + }); + } + + void test_files(const dpp::message &message) { + set_test(MESSAGEFILE, false); + if (message.attachments.size() == 3) { + static constexpr auto check_mimetype = [](const auto &headers, std::string mimetype) { + if (auto it = headers.find("content-type"); it != headers.end()) { + // check that the mime type starts with what we gave : for example discord will change "text/plain" to "text/plain; charset=UTF-8" + return it->second.size() >= mimetype.size() && std::equal(it->second.begin(), it->second.begin() + mimetype.size(), mimetype.begin()); + } + else { + return false; + } + }; + message.attachments[0].download([&](const dpp::http_request_completion_t &callback) { + std::lock_guard lock(mutex); + if (callback.status == 200 && callback.body == "test") { + files_success[0] = true; + } + set_file_tested(0); + }); + message.attachments[1].download([&](const dpp::http_request_completion_t &callback) { + std::lock_guard lock(mutex); + if (callback.status == 200 && check_mimetype(callback.headers, "text/plain") && callback.body == "test") { + files_success[1] = true; + } + set_file_tested(1); + }); + message.attachments[2].download([&](const dpp::http_request_completion_t &callback) { + std::lock_guard lock(mutex); + // do not check the contents here because discord can change compression + if (callback.status == 200 && check_mimetype(callback.headers, "image/png")) { + files_success[2] = true; + } + set_file_tested(2); + }); + } + else { + set_file_tested(0); + set_file_tested(1); + set_file_tested(2); + } + } + + void test_pin() { + if (!extended) { + set_pin_tested(); + return; + } + set_test(MESSAGEPIN, false); + set_test(MESSAGEUNPIN, false); + bot.message_pin(channel_id, message_id, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (!callback.is_error()) { + set_test(MESSAGEPIN, true); + bot.message_unpin(TEST_TEXT_CHANNEL_ID, message_id, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (!callback.is_error()) { + set_test(MESSAGEUNPIN, true); + } + set_pin_tested(); + }); + } + else { + set_pin_tested(); + } + }); + } + + public: + dpp::snowflake thread_id; + + message_test_helper(dpp::cluster &_bot) : bot(_bot) {} + + void run(const dpp::message &message) { + pin_tested = false; + thread_tested = false; + files_tested = {false, false, false}; + files_success = {false, false, false}; + channel_id = message.channel_id; + message_id = message.id; + test_pin(); + test_files(message); + test_threads(message); + } + }; + + message_test_helper message_helper(bot); + + class thread_test_helper + { + public: + enum event_flag + { + MESSAGE_CREATE = 1 << 0, + MESSAGE_EDIT = 1 << 1, + MESSAGE_REACT = 1 << 2, + MESSAGE_REMOVE_REACT = 1 << 3, + MESSAGE_DELETE = 1 << 4, + EVENT_END = 1 << 5 + }; + private: + std::mutex mutex; + dpp::cluster ⊥ + bool edit_tested = false; + bool members_tested = false; + bool messages_tested = false; + bool events_tested = false; + bool get_active_tested = false; + uint32_t events_tested_mask = 0; + uint32_t events_to_test_mask = 0; + + void delete_if_done() + { + if (edit_tested && members_tested && messages_tested && events_tested && get_active_tested) { + bot.channel_delete(thread_id); + } + } + + void set_events_tested() + { + if (events_tested) { + return; + } + events_tested = true; + delete_if_done(); + } + + void set_edit_tested() + { + if (edit_tested) { + return; + } + edit_tested = true; + delete_if_done(); + } + + void set_members_tested() + { + if (members_tested) { + return; + } + members_tested = true; + delete_if_done(); + } + + void set_get_active_tested() + { + if (get_active_tested) { + return; + } + get_active_tested = true; + delete_if_done(); + } + + void set_messages_tested() + { + if (messages_tested) { + return; + } + messages_tested = true; + delete_if_done(); + } + + void set_event_tested(event_flag flag) + { + if (events_tested_mask & flag) { + return; + } + events_tested_mask |= flag; + for (uint32_t i = 1; i < EVENT_END; i <<= 1) { + if ((events_to_test_mask & i) && (events_tested_mask & i) != i) { + return; + } + } + set_events_tested(); + } + + void events_abort() + { + events_tested_mask |= ~events_to_test_mask; + for (uint32_t i = 1; i < EVENT_END; i <<= 1) { + if ((events_tested_mask & i) != i) { + return; + } + } + set_events_tested(); + } + + public: + /** + * @Brief wrapper for set_event_tested, locking the mutex. Meant to be used from outside the class + */ + void notify_event_tested(event_flag flag) + { + std::lock_guard lock{mutex}; + + set_event_tested(flag); + } + + dpp::snowflake thread_id; + + void test_edit(const dpp::thread &thread) + { + std::lock_guard lock{mutex}; + + if (!edit_tested) { + dpp::thread edit = thread; + set_test(THREAD_EDIT, false); + set_test(THREAD_UPDATE_EVENT, false); + edit.name = "edited"; + edit.metadata.locked = true; + bot.thread_edit(edit, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock(mutex); + if (!callback.is_error()) { + set_test(THREAD_EDIT, true); + } + set_edit_tested(); + }); + } + } + + void test_get_active(const dpp::thread &thread) + { + std::lock_guard lock{mutex}; + + set_test(THREAD_GET_ACTIVE, false); + bot.threads_get_active(TEST_GUILD_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (!callback.is_error()) { + const auto &threads = callback.get(); + if (auto thread_it = threads.find(thread_id); thread_it != threads.end()) { + const auto &thread = thread_it->second.active_thread; + const auto &member = thread_it->second.bot_member; + if (thread.id == thread_id && member.has_value() && member->user_id == bot.me.id) { + set_test(THREAD_GET_ACTIVE, true); + } + } + } + set_get_active_tested(); + }); + } + + void test_members(const dpp::thread &thread) + { + std::lock_guard lock{mutex}; + + if (!members_tested) { + if (!extended) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBER_ADD, false); + set_test(THREAD_MEMBER_GET, false); + set_test(THREAD_MEMBERS_GET, false); + set_test(THREAD_MEMBER_REMOVE, false); + set_test(THREAD_MEMBERS_ADD_EVENT, false); + set_test(THREAD_MEMBERS_REMOVE_EVENT, false); + bot.thread_member_add(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBER_ADD, true); + bot.thread_member_get(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBER_GET, true); + bot.thread_members_get(thread_id, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + set_members_tested(); + return; + } + const auto &members = callback.get(); + if (members.find(TEST_USER_ID) == members.end() || members.find(bot.me.id) == members.end()) { + set_members_tested(); + return; + } + set_test(THREAD_MEMBERS_GET, true); + bot.thread_member_remove(thread_id, TEST_USER_ID, [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (!callback.is_error()) { + set_test(THREAD_MEMBER_REMOVE, true); + } + set_members_tested(); + }); + }); + }); + }); + } + } + + void test_messages(const dpp::thread &thread) + { + if (!extended) { + set_messages_tested(); + set_events_tested(); + return; + } + + std::lock_guard lock{mutex}; + set_test(THREAD_MESSAGE, false); + set_test(THREAD_MESSAGE_CREATE_EVENT, false); + set_test(THREAD_MESSAGE_EDIT_EVENT, false); + set_test(THREAD_MESSAGE_REACT_ADD_EVENT, false); + set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, false); + set_test(THREAD_MESSAGE_DELETE_EVENT, false); + events_to_test_mask |= MESSAGE_CREATE; + bot.message_create(dpp::message{"hello thread"}.set_channel_id(thread.id), [this](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + auto m = callback.get(); + m.content = "hello thread?"; + events_to_test_mask |= MESSAGE_EDIT; + bot.message_edit(m, [this, message_id = m.id](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + events_to_test_mask |= MESSAGE_REACT; + bot.message_add_reaction(message_id, thread_id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + events_to_test_mask |= MESSAGE_REMOVE_REACT; + bot.message_delete_reaction(message_id, thread_id, bot.me.id, dpp::unicode_emoji::thread, [this, message_id](const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + if (callback.is_error()) { + events_abort(); + set_messages_tested(); + return; + } + events_to_test_mask |= MESSAGE_DELETE; + bot.message_delete(message_id, thread_id, [this] (const dpp::confirmation_callback_t &callback) { + std::lock_guard lock{mutex}; + set_messages_tested(); + if (callback.is_error()) { + events_abort(); + return; + } + set_test(THREAD_MESSAGE, true); + }); + }); + }); + }); + }); + } + + void run(const dpp::thread &thread) + { + thread_id = thread.id; + test_get_active(thread); + test_edit(thread); + test_members(thread); + test_messages(thread); + } + + thread_test_helper(dpp::cluster &bot_) : bot{bot_} + { + } + }; + + thread_test_helper thread_helper(bot); + + bot.on_thread_create([&](const dpp::thread_create_t &event) { + if (event.created.name == "thread test") { + set_test(THREAD_CREATE_EVENT, true); + thread_helper.run(event.created); + } + }); + + bool message_tested = false; + bot.on_message_create([&](const dpp::message_create_t & event) { + if (event.msg.author.id == bot.me.id) { + if (event.msg.content == "test message" && !message_tested) { + message_tested = true; + set_test(MESSAGERECEIVE, true); + message_helper.run(event.msg); + set_test(MESSAGESGET, false); + bot.messages_get(event.msg.channel_id, 0, event.msg.id, 0, 5, [](const dpp::confirmation_callback_t &cc){ + if (!cc.is_error()) { + dpp::message_map mm = std::get(cc.value); + if (mm.size()) { + set_test(MESSAGESGET, true); + set_test(TIMESTAMP, false); + dpp::message m = mm.begin()->second; + if (m.sent > 0) { + set_test(TIMESTAMP, true); + } else { + set_test(TIMESTAMP, false); + } + } else { + set_test(MESSAGESGET, false); + } + } else { + set_test(MESSAGESGET, false); + } + }); + set_test(MSGCREATESEND, false); + event.send("MSGCREATESEND", [&bot, ch_id = event.msg.channel_id] (const auto& cc) { + if (!cc.is_error()) { + dpp::message m = std::get(cc.value); + if (m.channel_id == ch_id) { + set_test(MSGCREATESEND, true); + } else { + bot.log(dpp::ll_debug, cc.http_info.body); + set_test(MSGCREATESEND, false); + } + bot.message_delete(m.id, m.channel_id); + } else { + bot.log(dpp::ll_debug, cc.http_info.body); + set_test(MSGCREATESEND, false); + } + }); + } + if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread") { + set_test(THREAD_MESSAGE_CREATE_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_CREATE); + } + } + }); + + bot.on_message_reaction_add([&](const dpp::message_reaction_add_t & event) { + if (event.reacting_user.id == bot.me.id) { + if (event.reacting_emoji.name == "😄") { + set_test(REACTEVENT, true); + } + if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { + set_test(THREAD_MESSAGE_REACT_ADD_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REACT); + } + } + }); + + bot.on_message_reaction_remove([&](const dpp::message_reaction_remove_t & event) { + if (event.reacting_user_id == bot.me.id) { + if (event.channel_id == thread_helper.thread_id && event.reacting_emoji.name == dpp::unicode_emoji::thread) { + set_test(THREAD_MESSAGE_REACT_REMOVE_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_REMOVE_REACT); + } + } + }); + + bot.on_message_delete([&](const dpp::message_delete_t & event) { + if (event.channel_id == thread_helper.thread_id) { + set_test(THREAD_MESSAGE_DELETE_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_DELETE); + } + }); + + bool message_edit_tested = false; + bot.on_message_update([&](const dpp::message_update_t &event) { + if (event.msg.author == bot.me.id) { + if (event.msg.content == "test edit" && !message_edit_tested) { + message_edit_tested = true; + set_test(EDITEVENT, true); + } + if (event.msg.channel_id == thread_helper.thread_id && event.msg.content == "hello thread?") { + set_test(THREAD_MESSAGE_EDIT_EVENT, true); + thread_helper.notify_event_tested(thread_test_helper::MESSAGE_EDIT); + } + } + }); + + bot.on_thread_update([&](const dpp::thread_update_t &event) { + if (event.updating_guild->id == TEST_GUILD_ID && event.updated.id == thread_helper.thread_id && event.updated.name == "edited") { + set_test(THREAD_UPDATE_EVENT, true); + } + }); + + bot.on_thread_members_update([&](const dpp::thread_members_update_t &event) { + if (event.updating_guild->id == TEST_GUILD_ID && event.thread_id == thread_helper.thread_id) { + if (std::find_if(std::begin(event.added), std::end(event.added), is_owner) != std::end(event.added)) { + set_test(THREAD_MEMBERS_ADD_EVENT, true); + } + if (std::find_if(std::begin(event.removed_ids), std::end(event.removed_ids), is_owner) != std::end(event.removed_ids)) { + set_test(THREAD_MEMBERS_REMOVE_EVENT, true); + } + } + }); + + bot.on_thread_delete([&](const dpp::thread_delete_t &event) { + if (event.deleting_guild->id == TEST_GUILD_ID && event.deleted.id == message_helper.thread_id) { + set_test(THREAD_DELETE_EVENT, true); + } + }); + + // set to execute from this thread (main thread) after on_ready is fired + auto do_online_tests = [&] { + coro_online_tests(&bot); + set_test(GUILD_BAN_CREATE, false); + set_test(GUILD_BAN_GET, false); + set_test(GUILD_BANS_GET, false); + set_test(GUILD_BAN_DELETE, false); + if (!offline) { + // some deleted discord accounts to test the ban stuff with... + dpp::snowflake deadUser1(802670069523415057); + dpp::snowflake deadUser2(875302419335094292); + dpp::snowflake deadUser3(1048247361903792198); + + bot.set_audit_reason("ban reason one").guild_ban_add(TEST_GUILD_ID, deadUser1, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) bot.guild_ban_add(TEST_GUILD_ID, deadUser2, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) bot.set_audit_reason("ban reason three").guild_ban_add(TEST_GUILD_ID, deadUser3, 0, [deadUser1, deadUser2, deadUser3, &bot](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + set_test(GUILD_BAN_CREATE, true); + // when created, continue with getting and deleting + + // get ban + bot.guild_get_ban(TEST_GUILD_ID, deadUser1, [deadUser1](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + dpp::ban ban = event.get(); + if (ban.user_id == deadUser1 && ban.reason == "ban reason one") { + set_test(GUILD_BAN_GET, true); + } + } + }); + + // get multiple bans + bot.guild_get_bans(TEST_GUILD_ID, 0, deadUser1, 3, [deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + dpp::ban_map bans = event.get(); + int successCount = 0; + for (auto &ban: bans) { + if (ban.first == ban.second.user_id) { // the key should match the ban's user_id + if (ban.first == deadUser2 && ban.second.reason.empty()) { + successCount++; + } else if (ban.first == deadUser3 && ban.second.reason == "ban reason three") { + successCount++; + } + } + } + if (successCount == 2) { + set_test(GUILD_BANS_GET, true); + } + } + }); + + // unban them + bot.guild_ban_delete(TEST_GUILD_ID, deadUser1, [&bot, deadUser2, deadUser3](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + bot.guild_ban_delete(TEST_GUILD_ID, deadUser2, [&bot, deadUser3](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + bot.guild_ban_delete(TEST_GUILD_ID, deadUser3, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(GUILD_BAN_DELETE, true); + } + }); + } + }); + } + }); + }); + }); + }); + } + + set_test(REQUEST_GET_IMAGE, false); + if (!offline) { + bot.request("https://dpp.dev/DPP-Logo.png", dpp::m_get, [&bot](const dpp::http_request_completion_t &callback) { + if (callback.status != 200) { + return; + } + set_test(REQUEST_GET_IMAGE, true); + + dpp::emoji emoji; + emoji.load_image(callback.body, dpp::i_png); + emoji.name = "dpp"; + + // emoji unit test with the requested image + set_test(EMOJI_CREATE, false); + set_test(EMOJI_GET, false); + set_test(EMOJI_DELETE, false); + bot.guild_emoji_create(TEST_GUILD_ID, emoji, [&bot](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + set_test(EMOJI_CREATE, true); + + auto created = event.get(); + bot.guild_emoji_get(TEST_GUILD_ID, created.id, [&bot, created](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto fetched = event.get(); + if (created.id == fetched.id && created.name == fetched.name && created.flags == fetched.flags) { + set_test(EMOJI_GET, true); + } + + bot.guild_emoji_delete(TEST_GUILD_ID, fetched.id, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(EMOJI_DELETE, true); + } + }); + }); + }); + }); + } + + set_test(INVITE_CREATE, false); + set_test(INVITE_GET, false); + set_test(INVITE_DELETE, false); + if (!offline) { + dpp::channel channel; + channel.id = TEST_TEXT_CHANNEL_ID; + dpp::invite invite; + invite.max_age = 0; + invite.max_uses = 100; + set_test(INVITE_CREATE_EVENT, false); + bot.channel_invite_create(channel, invite, [&bot, invite](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + + auto created = event.get(); + if (!created.code.empty() && created.channel_id == TEST_TEXT_CHANNEL_ID && created.guild_id == TEST_GUILD_ID && created.inviter.id == bot.me.id) { + set_test(INVITE_CREATE, true); + } + + bot.invite_get(created.code, [&bot, created](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + auto retrieved = event.get(); + if (retrieved.code == created.code && retrieved.guild_id == created.guild_id && retrieved.channel_id == created.channel_id && retrieved.inviter.id == created.inviter.id) { + if (retrieved.destination_guild.flags & dpp::g_community) { + set_test(INVITE_GET, retrieved.expires_at == 0); + } else { + set_test(INVITE_GET, true); + } + + } else { + set_test(INVITE_GET, false); + } + } else { + set_test(INVITE_GET, false); + } + + set_test(INVITE_DELETE_EVENT, false); + bot.invite_delete(created.code, [](const dpp::confirmation_callback_t &event) { + set_test(INVITE_DELETE, !event.is_error()); + }); + }); + }); + } + + set_test(AUTOMOD_RULE_CREATE, false); + set_test(AUTOMOD_RULE_GET, false); + set_test(AUTOMOD_RULE_GET_ALL, false); + set_test(AUTOMOD_RULE_DELETE, false); + if (!offline) { + dpp::automod_rule automodRule; + automodRule.name = "automod rule (keyword type)"; + automodRule.trigger_type = dpp::amod_type_keyword; + dpp::automod_metadata metadata1; + metadata1.keywords.emplace_back("*cat*"); + metadata1.keywords.emplace_back("train"); + metadata1.keywords.emplace_back("*.exe"); + metadata1.regex_patterns.emplace_back("^[^a-z]$"); + metadata1.allow_list.emplace_back("@silent*"); + automodRule.trigger_metadata = metadata1; + dpp::automod_action automodAction; + automodAction.type = dpp::amod_action_timeout; + automodAction.duration_seconds = 6000; + automodRule.actions.emplace_back(automodAction); + + bot.automod_rules_get(TEST_GUILD_ID, [&bot, automodRule](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto rules = event.get(); + set_test(AUTOMOD_RULE_GET_ALL, true); + for (const auto &rule: rules) { + if (rule.second.trigger_type == dpp::amod_type_keyword) { + // delete one automod rule of type KEYWORD before creating one to make space... + bot.automod_rule_delete(TEST_GUILD_ID, rule.first); + } + } + + // start creating the automod rules + bot.automod_rule_create(TEST_GUILD_ID, automodRule, [&bot, automodRule](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto created = event.get(); + if (created.name == automodRule.name) { + set_test(AUTOMOD_RULE_CREATE, true); + } + + // get automod rule + bot.automod_rule_get(TEST_GUILD_ID, created.id, [automodRule, &bot, created](const dpp::confirmation_callback_t &event) { + if (event.is_error()) { + return; + } + auto retrieved = event.get(); + if (retrieved.name == automodRule.name && + retrieved.trigger_type == automodRule.trigger_type && + retrieved.trigger_metadata.keywords == automodRule.trigger_metadata.keywords && + retrieved.trigger_metadata.regex_patterns == automodRule.trigger_metadata.regex_patterns && + retrieved.trigger_metadata.allow_list == automodRule.trigger_metadata.allow_list && retrieved.actions.size() == automodRule.actions.size()) { + set_test(AUTOMOD_RULE_GET, true); + } + + // delete the automod rule + bot.automod_rule_delete(TEST_GUILD_ID, retrieved.id, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(AUTOMOD_RULE_DELETE, true); + } + }); + }); + }); + }); + } + + set_test(USER_GET, false); + set_test(USER_GET_FLAGS, false); + if (!offline) { + bot.user_get(TEST_USER_ID, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + auto u = std::get(event.value); + if (u.id == TEST_USER_ID) { + set_test(USER_GET, true); + } else { + set_test(USER_GET, false); + } + json j = json::parse(event.http_info.body); + uint64_t raw_flags = j["public_flags"]; + if (j.contains("flags")) { + uint64_t flags = j["flags"]; + raw_flags |= flags; + } + // testing all user flags from https://discord.com/developers/docs/resources/user#user-object-user-flags + // they're manually set here because the dpp::user_flags don't match to the discord API, so we can't use them to compare with the raw flags! + if ( + u.is_discord_employee() == ((raw_flags & (1 << 0)) != 0) && + u.is_partnered_owner() == ((raw_flags & (1 << 1)) != 0) && + u.has_hypesquad_events() == ((raw_flags & (1 << 2)) != 0) && + u.is_bughunter_1() == ((raw_flags & (1 << 3)) != 0) && + u.is_house_bravery() == ((raw_flags & (1 << 6)) != 0) && + u.is_house_brilliance() == ((raw_flags & (1 << 7)) != 0) && + u.is_house_balance() == ((raw_flags & (1 << 8)) != 0) && + u.is_early_supporter() == ((raw_flags & (1 << 9)) != 0) && + u.is_team_user() == ((raw_flags & (1 << 10)) != 0) && + u.is_bughunter_2() == ((raw_flags & (1 << 14)) != 0) && + u.is_verified_bot() == ((raw_flags & (1 << 16)) != 0) && + u.is_verified_bot_dev() == ((raw_flags & (1 << 17)) != 0) && + u.is_certified_moderator() == ((raw_flags & (1 << 18)) != 0) && + u.is_bot_http_interactions() == ((raw_flags & (1 << 19)) != 0) && + u.is_active_developer() == ((raw_flags & (1 << 22)) != 0) + ) { + set_test(USER_GET_FLAGS, true); + } else { + set_test(USER_GET_FLAGS, false); + } + } else { + set_test(USER_GET, false); + set_test(USER_GET_FLAGS, false); + } + }); + } + + set_test(VOICE_CHANNEL_CREATE, false); + set_test(VOICE_CHANNEL_EDIT, false); + set_test(VOICE_CHANNEL_DELETE, false); + if (!offline) { + dpp::channel channel1; + channel1.set_type(dpp::CHANNEL_VOICE) + .set_guild_id(TEST_GUILD_ID) + .set_name("voice1") + .add_permission_overwrite(TEST_GUILD_ID, dpp::ot_role, 0, dpp::p_view_channel) + .set_user_limit(99); + dpp::channel createdChannel; + try { + createdChannel = bot.channel_create_sync(channel1); + } catch (dpp::rest_exception &exception) { + set_test(VOICE_CHANNEL_CREATE, false); + } + if (createdChannel.name == channel1.name && + createdChannel.user_limit == 99 && + createdChannel.name == "voice1") { + for (auto overwrite: createdChannel.permission_overwrites) { + if (overwrite.id == TEST_GUILD_ID && overwrite.type == dpp::ot_role && overwrite.deny == dpp::p_view_channel) { + set_test(VOICE_CHANNEL_CREATE, true); + } + } + + // edit the voice channel + createdChannel.set_name("foobar2"); + createdChannel.set_user_limit(2); + for (auto overwrite: createdChannel.permission_overwrites) { + if (overwrite.id == TEST_GUILD_ID) { + overwrite.deny.set(0); + overwrite.allow.set(dpp::p_view_channel); + } + } + try { + dpp::channel edited = bot.channel_edit_sync(createdChannel); + if (edited.name == "foobar2" && edited.user_limit == 2) { + set_test(VOICE_CHANNEL_EDIT, true); + } + } catch (dpp::rest_exception &exception) { + set_test(VOICE_CHANNEL_EDIT, false); + } + + // delete the voice channel + try { + bot.channel_delete_sync(createdChannel.id); + set_test(VOICE_CHANNEL_DELETE, true); + } catch (dpp::rest_exception &exception) { + set_test(VOICE_CHANNEL_DELETE, false); + } + } + } + + set_test(FORUM_CREATION, false); + set_test(FORUM_CHANNEL_GET, false); + set_test(FORUM_CHANNEL_DELETE, false); + if (!offline) { + dpp::channel c; + c.name = "test-forum-channel"; + c.guild_id = TEST_GUILD_ID; + c.set_topic("This is a forum channel"); + c.set_flags(dpp::CHANNEL_FORUM); + c.default_sort_order = dpp::so_creation_date; + dpp::forum_tag t; + t.name = "Alpha"; + t.emoji = "❌"; + c.available_tags = {t}; + c.default_auto_archive_duration = dpp::arc_1_day; + c.default_reaction = "✅"; + c.default_thread_rate_limit_per_user = 10; + bot.channel_create(c, [&bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(FORUM_CREATION, true); + auto channel = std::get(event.value); + // retrieve the forum channel and check the values + bot.channel_get(channel.id, [forum_id = channel.id, &bot](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + auto channel = std::get(event.value); + bot.log(dpp::ll_debug, event.http_info.body); + bool tag = false; + for (auto &t : channel.available_tags) { + if (t.name == "Alpha" && std::holds_alternative(t.emoji) && std::get(t.emoji) == "❌") { + tag = true; + } + } + bool name = channel.name == "test-forum-channel"; + bool sort = channel.default_sort_order == dpp::so_creation_date; + bool rateLimit = channel.default_thread_rate_limit_per_user == 10; + set_test(FORUM_CHANNEL_GET, tag && name && sort && rateLimit); + } else { + set_test(FORUM_CHANNEL_GET, false); + } + // delete the forum channel + bot.channel_delete(forum_id, [](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + set_test(FORUM_CHANNEL_DELETE, true); + } else { + set_test(FORUM_CHANNEL_DELETE, false); + } + }); + }); + } else { + set_test(FORUM_CREATION, false); + set_test(FORUM_CHANNEL_GET, false); + } + }); + } + + set_test(THREAD_CREATE, false); + if (!offline) { + bot.thread_create("thread test", TEST_TEXT_CHANNEL_ID, 60, dpp::channel_type::CHANNEL_PUBLIC_THREAD, true, 60, [&](const dpp::confirmation_callback_t &event) { + if (!event.is_error()) { + [[maybe_unused]] const auto &thread = event.get(); + set_test(THREAD_CREATE, true); + } + // the thread tests are in the on_thread_create event handler + }); + } + + set_test(MEMBER_GET, false); + if (!offline) { + bot.guild_get_member(TEST_GUILD_ID, TEST_USER_ID, [](const dpp::confirmation_callback_t &event){ + if (!event.is_error()) { + dpp::guild_member m = std::get(event.value); + if (m.guild_id == TEST_GUILD_ID && m.user_id == TEST_USER_ID) { + set_test(MEMBER_GET, true); + } else { + set_test(MEMBER_GET, false); + } + } else { + set_test(MEMBER_GET, false); + } + }); + } + + set_test(ROLE_CREATE, false); + set_test(ROLE_EDIT, false); + set_test(ROLE_DELETE, false); + if (!offline) { + dpp::role r; + r.guild_id = TEST_GUILD_ID; + r.name = "Test-Role"; + r.permissions.add(dpp::p_move_members); + r.set_flags(dpp::r_mentionable); + r.colour = dpp::colors::moon_yellow; + dpp::role createdRole; + try { + createdRole = bot.role_create_sync(r); + if (createdRole.name == r.name && + createdRole.has_move_members() && + createdRole.flags & dpp::r_mentionable && + createdRole.colour == r.colour) { + set_test(ROLE_CREATE, true); + } + } catch (dpp::rest_exception &exception) { + set_test(ROLE_CREATE, false); + } + createdRole.guild_id = TEST_GUILD_ID; + createdRole.name = "Test-Role-Edited"; + createdRole.colour = dpp::colors::light_sea_green; + try { + dpp::role edited = bot.role_edit_sync(createdRole); + if (createdRole.id == edited.id && edited.name == "Test-Role-Edited") { + set_test(ROLE_EDIT, true); + } + } catch (dpp::rest_exception &exception) { + set_test(ROLE_EDIT, false); + } + try { + bot.role_delete_sync(TEST_GUILD_ID, createdRole.id); + set_test(ROLE_DELETE, true); + } catch (dpp::rest_exception &exception) { + set_test(ROLE_DELETE, false); + } + } + }; + + set_test(BOTSTART, false); + try { + if (!offline) { + bot.start(true); + set_test(BOTSTART, true); + } + } + catch (const std::exception &) { + set_test(BOTSTART, false); + } + + set_test(TIMERSTART, false); + uint32_t ticks = 0; + dpp::timer th = bot.start_timer([&](dpp::timer timer_handle) { + if (ticks == 5) { + /* The simple test timer ticks every second. + * If we get to 5 seconds, we know the timer is working. + */ + set_test(TIMERSTART, true); + } + ticks++; + }, 1); + + set_test(USER_GET_CACHED_PRESENT, false); + try { + dpp::user_identified u = bot.user_get_cached_sync(TEST_USER_ID); + set_test(USER_GET_CACHED_PRESENT, (u.id == TEST_USER_ID)); + } + catch (const std::exception&) { + set_test(USER_GET_CACHED_PRESENT, false); + } + + set_test(USER_GET_CACHED_ABSENT, false); + try { + /* This is the snowflake ID of a discord staff member. + * We assume here that staffer's discord IDs will remain constant + * for long periods of time and they won't lurk in the unit test server. + * If this becomes not true any more, we'll pick another well known + * user ID. + */ + dpp::user_identified u = bot.user_get_cached_sync(90339695967350784); + set_test(USER_GET_CACHED_ABSENT, (u.id == dpp::snowflake(90339695967350784))); + } + catch (const std::exception&) { + set_test(USER_GET_CACHED_ABSENT, false); + } + + set_test(TIMEDLISTENER, false); + dpp::timed_listener tl(&bot, 10, bot.on_log, [&](const dpp::log_t & event) { + set_test(TIMEDLISTENER, true); + }); + + set_test(ONESHOT, false); + bool once = false; + dpp::oneshot_timer ost(&bot, 5, [&](dpp::timer timer_handle) { + if (!once) { + set_test(ONESHOT, true); + } else { + set_test(ONESHOT, false); + } + once = true; + }); + + set_test(CUSTOMCACHE, false); + dpp::cache testcache; + test_cached_object_t* tco = new test_cached_object_t(666); + tco->foo = "bar"; + testcache.store(tco); + test_cached_object_t* found_tco = testcache.find(666); + if (found_tco && found_tco->id == dpp::snowflake(666) && found_tco->foo == "bar") { + set_test(CUSTOMCACHE, true); + } else { + set_test(CUSTOMCACHE, false); + } + testcache.remove(found_tco); + + if (!offline) { + if (std::future_status status = ready_future.wait_for(std::chrono::seconds(20)); status != std::future_status::timeout) { + do_online_tests(); + } + } + + noparam_api_test(current_user_get, dpp::user_identified, CURRENTUSER); + singleparam_api_test(channel_get, TEST_TEXT_CHANNEL_ID, dpp::channel, GETCHAN); + singleparam_api_test(guild_get, TEST_GUILD_ID, dpp::guild, GETGUILD); + singleparam_api_test_list(roles_get, TEST_GUILD_ID, dpp::role_map, GETROLES); + singleparam_api_test_list(channels_get, TEST_GUILD_ID, dpp::channel_map, GETCHANS); + singleparam_api_test_list(guild_get_invites, TEST_GUILD_ID, dpp::invite_map, GETINVS); + multiparam_api_test_list(guild_get_bans, TEST_GUILD_ID, dpp::ban_map, GETBANS); + singleparam_api_test_list(channel_pins_get, TEST_TEXT_CHANNEL_ID, dpp::message_map, GETPINS); + singleparam_api_test_list(guild_events_get, TEST_GUILD_ID, dpp::scheduled_event_map, GETEVENTS); + twoparam_api_test(guild_event_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::scheduled_event, GETEVENT); + twoparam_api_test_list(guild_event_users_get, TEST_GUILD_ID, TEST_EVENT_ID, dpp::event_member_map, GETEVENTUSERS); + + std::this_thread::sleep_for(std::chrono::seconds(20)); + + /* Test stopping timer */ + set_test(TIMERSTOP, false); + set_test(TIMERSTOP, bot.stop_timer(th)); + + set_test(USERCACHE, false); + if (!offline) { + dpp::user* u = dpp::find_user(TEST_USER_ID); + set_test(USERCACHE, u); + } + set_test(CHANNELCACHE, false); + set_test(CHANNELTYPES, false); + if (!offline) { + dpp::channel* c = dpp::find_channel(TEST_TEXT_CHANNEL_ID); + dpp::channel* c2 = dpp::find_channel(TEST_VC_ID); + set_test(CHANNELCACHE, c && c2); + set_test(CHANNELTYPES, c && c->is_text_channel() && !c->is_voice_channel() && c2 && c2->is_voice_channel() && !c2->is_text_channel()); + } + + wait_for_tests(); + + } + catch (const std::exception &e) { + std::cout << e.what() << "\n"; + set_test(CLUSTER, false); + } + /* Return value = number of failed tests, exit code 0 = success */ return test_summary(); } diff --git a/src/unittest/test.h b/src/unittest/test.h index 343a0efb15..7b2ee53e8b 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -569,33 +569,3 @@ inline constexpr auto is_owner = [](auto &&user) noexcept { DPP_CHECK(test, std::is_copy_assignable_v, var); \ DPP_CHECK(test, std::is_move_assignable_v, var); \ } while(0) - -/** - * @brief Unit tests for Human readable error translation - */ -void errors_test(); - -/** -* @brief Unit tests for HTTPS client -*/ -void http_client_tests(const std::string&); - -/** -* @brief Unit tests for Discord objects (webhook, interaction, user etc.) -*/ -void discord_objects_tests(); - -/** - * @brief Unit tests for Gateway events - */ -void gateway_events_tests(const std::string&, dpp::cluster&); - -/** -* @brief Unit tests for Cache -*/ -void cache_tests(dpp::cluster&); - -/** -* @brief Unit tests for library Utilities -*/ -void utility_tests(); diff --git a/src/unittest/utilities.cpp b/src/unittest/utilities.cpp deleted file mode 100644 index 5dbf37060d..0000000000 --- a/src/unittest/utilities.cpp +++ /dev/null @@ -1,190 +0,0 @@ -/************************************************************************************ - * - * D++, A Lightweight C++ library for Discord - * - * SPDX-License-Identifier: Apache-2.0 - * Copyright 2021 Craig Edwards and D++ contributors - * (https://github.com/brainboxdotcc/DPP/graphs/contributors) - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - * - ************************************************************************************/ -#include "test.h" - -#include - -/** - * @brief Type trait to check if a certain type has a build_json method - * - * @tparam T type to check for - */ -template > -struct has_build_json : std::false_type {}; - -template -struct has_build_json().build_json())>> : std::true_type {}; - -/** - * @brief Type trait to check if a certain type has a build_json method - * - * @tparam T type to check for - */ -template -constexpr bool has_build_json_v = has_build_json::value; - -/** - * @brief Type trait to check if a certain type has a fill_from_json method - * - * @tparam T type to check for - */ -template -struct has_fill_from_json : std::false_type {}; - -template -struct has_fill_from_json().fill_from_json(std::declval()))>> : std::true_type {}; - -/** - * @brief Type trait to check if a certain type has a fill_from_json method - * - * @tparam T type to check for - */ -template -constexpr bool has_fill_from_json_v = has_fill_from_json::value; - -/* Unit tests for library utilities */ -void utility_tests() { - // markdown escape tests - std::string test_to_escape = "*** _This is a test_ ***\n```cpp\n\ -int main() {\n\ - /* Comment */\n\ - int answer = 42;\n\ - return answer; // ___\n\ -};\n\ -```\n\ -Markdown lol ||spoiler|| ~~strikethrough~~ `small *code* block`\n"; - - set_test(MD_ESC_1, false); - set_test(MD_ESC_2, false); - std::string escaped1 = dpp::utility::markdown_escape(test_to_escape); - std::string escaped2 = dpp::utility::markdown_escape(test_to_escape, true); - set_test(MD_ESC_1, escaped1 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ -```cpp\n\ -int main() {\n\ - /* Comment */\n\ - int answer = 42;\n\ - return answer; // ___\n\ -};\n\ -```\n\ -Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ `small *code* block`\n"); - set_test(MD_ESC_2, escaped2 == "\\*\\*\\* \\_This is a test\\_ \\*\\*\\*\n\ -\\`\\`\\`cpp\n\ -int main\\(\\) {\n\ - /\\* Comment \\*/\n\ - int answer = 42;\n\ - return answer; // \\_\\_\\_\n\ -};\n\ -\\`\\`\\`\n\ -Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* block\\`\n"); - - - set_test(COMPARISON, false); - dpp::user u1; - dpp::user u2; - dpp::user u3; - u1.id = u2.id = 666; - u3.id = 777; - set_test(COMPARISON, u1 == u2 && u1 != u3); - - // encoding tests - set_test(URLENC, false); - set_test(URLENC, dpp::utility::url_encode("ABC123_+\\|$*/AAA[]😄") == "ABC123_%2B%5C%7C%24%2A%2FAAA%5B%5D%F0%9F%98%84"); - - set_test(BASE64ENC, false); - set_test(BASE64ENC, - dpp::base64_encode(reinterpret_cast("a"), 1) == "YQ==" && - dpp::base64_encode(reinterpret_cast("bc"), 2) == "YmM=" && - dpp::base64_encode(reinterpret_cast("def"), 3) == "ZGVm" && - dpp::base64_encode(reinterpret_cast("ghij"), 4) == "Z2hpag==" && - dpp::base64_encode(reinterpret_cast("klmno"), 5) == "a2xtbm8=" && - dpp::base64_encode(reinterpret_cast("pqrstu"), 6) == "cHFyc3R1" && - dpp::base64_encode(reinterpret_cast("vwxyz12"), 7) == "dnd4eXoxMg==" - ); - - set_test(READFILE, false); - std::string rf_test = dpp::utility::read_file(SHARED_OBJECT); - FILE* fp = fopen(SHARED_OBJECT, "rb"); - fseek(fp, 0, SEEK_END); - size_t off = (size_t)ftell(fp); - fclose(fp); - set_test(READFILE, off == rf_test.length()); - - set_test(TIMESTAMPTOSTRING, false); - set_test(TIMESTAMPTOSTRING, dpp::ts_to_string(1642611864) == "2022-01-19T17:04:24Z"); - -#ifndef _WIN32 - set_test(TIMESTRINGTOTIMESTAMP, false); - json tj; - tj["t1"] = "2022-01-19T17:18:14.506000+00:00"; - tj["t2"] = "2022-01-19T17:18:14+00:00"; - uint32_t inTimestamp = 1642612694; - set_test(TIMESTRINGTOTIMESTAMP, (uint64_t)dpp::ts_not_null(&tj, "t1") == inTimestamp && (uint64_t)dpp::ts_not_null(&tj, "t2") == inTimestamp); -#else - set_test(TIMESTRINGTOTIMESTAMP, true); -#endif - - { - set_test(TS, false); - dpp::managed m(189759562910400512); - set_test(TS, ((uint64_t) m.get_creation_time()) == 1465312605); - } - - { // test dpp::json_interface - start_test(JSON_INTERFACE); - struct fillable : dpp::json_interface { - fillable &fill_from_json_impl(dpp::json *) { - return *this; - } - }; - struct buildable : dpp::json_interface { - json to_json_impl(bool = false) const { - return {}; - } - }; - struct fillable_and_buildable : dpp::json_interface { - fillable_and_buildable &fill_from_json_impl(dpp::json *) { - return *this; - } - - json to_json_impl(bool = false) const { - return {}; - } - }; - bool success = true; - - DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); - DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); - DPP_CHECK(JSON_INTERFACE, !has_fill_from_json_v, success); - - DPP_CHECK(JSON_INTERFACE, !has_build_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); - DPP_CHECK(JSON_INTERFACE, !has_build_json_v, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); - - DPP_CHECK(JSON_INTERFACE, has_build_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v>, success); - DPP_CHECK(JSON_INTERFACE, has_build_json_v, success); - DPP_CHECK(JSON_INTERFACE, has_fill_from_json_v, success); - set_test(JSON_INTERFACE, success); - } -} From ed427becf2cda834e530b8f9ea0f49ea42d2539e Mon Sep 17 00:00:00 2001 From: Craig Edwards Date: Sun, 22 Oct 2023 13:51:32 +0000 Subject: [PATCH 09/14] missing runs --- include/dpp/cluster_coro_calls.h | 12 ++---------- include/dpp/cluster_sync_calls.h | 15 ++------------- src/dpp/cluster_coro_calls.cpp | 4 ++-- src/dpp/cluster_sync_calls.cpp | 4 ++-- 4 files changed, 8 insertions(+), 27 deletions(-) diff --git a/include/dpp/cluster_coro_calls.h b/include/dpp/cluster_coro_calls.h index 3a5d30680f..ddbb9a2395 100644 --- a/include/dpp/cluster_coro_calls.h +++ b/include/dpp/cluster_coro_calls.h @@ -2263,16 +2263,8 @@ */ [[nodiscard]] async co_guild_get_voice_regions(snowflake guild_id); -/** - * @brief Create a webhook - * @note This method supports audit log reasons set by the cluster::set_audit_reason() method. - * @see dpp::cluster::create_webhook - * @see https://discord.com/developers/docs/resources/webhook#create-webhook - * @param w Webhook to create - * @return webhook returned object on completion - * \memberof dpp::cluster - */ -[[nodiscard]] async co_create_webhook(const class webhook &w); + +[[nodiscard]] async co_create_webhook(const class webhook &wh); /** * @brief Delete a webhook diff --git a/include/dpp/cluster_sync_calls.h b/include/dpp/cluster_sync_calls.h index 6232c2bf4f..1594d8e0a4 100644 --- a/include/dpp/cluster_sync_calls.h +++ b/include/dpp/cluster_sync_calls.h @@ -2782,19 +2782,8 @@ voiceregion_map get_voice_regions_sync(); */ voiceregion_map guild_get_voice_regions_sync(snowflake guild_id); -/** - * @brief Create a webhook - * @note This method supports audit log reasons set by the cluster::set_audit_reason() method. - * @see dpp::cluster::create_webhook - * @see https://discord.com/developers/docs/resources/webhook#create-webhook - * @param w Webhook to create - * @return webhook returned object on completion - * \memberof dpp::cluster - * @throw dpp::rest_exception upon failure to execute REST function - * @warning This function is a blocking (synchronous) call and should only be used from within a separate thread. - * Avoid direct use of this function inside an event handler. - */ -webhook create_webhook_sync(const class webhook &w); + +webhook create_webhook_sync(const class webhook &wh); /** * @brief Delete a webhook diff --git a/src/dpp/cluster_coro_calls.cpp b/src/dpp/cluster_coro_calls.cpp index 9deb0d865c..528df234ab 100644 --- a/src/dpp/cluster_coro_calls.cpp +++ b/src/dpp/cluster_coro_calls.cpp @@ -747,8 +747,8 @@ async cluster::co_guild_get_voice_regions(snowflake gui return async{ this, static_cast(&cluster::guild_get_voice_regions), guild_id }; } -async cluster::co_create_webhook(const class webhook &w) { - return async{ this, static_cast(&cluster::create_webhook), w }; +async cluster::co_create_webhook(const class webhook &wh) { + return async{ this, static_cast(&cluster::create_webhook), wh }; } async cluster::co_delete_webhook(snowflake webhook_id) { diff --git a/src/dpp/cluster_sync_calls.cpp b/src/dpp/cluster_sync_calls.cpp index a05c19f2e9..0167edddb2 100644 --- a/src/dpp/cluster_sync_calls.cpp +++ b/src/dpp/cluster_sync_calls.cpp @@ -745,8 +745,8 @@ voiceregion_map cluster::guild_get_voice_regions_sync(snowflake guild_id) { return dpp::sync(this, static_cast(&cluster::guild_get_voice_regions), guild_id); } -webhook cluster::create_webhook_sync(const class webhook &w) { - return dpp::sync(this, static_cast(&cluster::create_webhook), w); +webhook cluster::create_webhook_sync(const class webhook &wh) { + return dpp::sync(this, static_cast(&cluster::create_webhook), wh); } confirmation cluster::delete_webhook_sync(snowflake webhook_id) { From 4744531fcf57b5a6114d4679b9e9e85549c7436b Mon Sep 17 00:00:00 2001 From: "Craig Edwards (Brain)" Date: Sun, 22 Oct 2023 16:47:15 +0100 Subject: [PATCH 10/14] fix, test: multiheader-cookie test, now queries a domain and url in our control which returns 3 hard coded test cookies (#968) --- src/unittest/test.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 086e0336f9..6775452153 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -286,7 +286,7 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b set_test(MULTIHEADER, false); try { - dpp::https_client c2("www.google.com", 80, "/", "GET", "", {}, true); + dpp::https_client c2("dl.dpp.dev", 443, "/cookietest.php", "GET", "", {}); size_t count = c2.get_header_count("set-cookie"); size_t count_list = c2.get_header_list("set-cookie").size(); // Google sets a bunch of cookies when we start accessing it. From e72ac767a952619272fb5b869fb3282811c5a550 Mon Sep 17 00:00:00 2001 From: Miuna <809711+Mishura4@users.noreply.github.com> Date: Sun, 22 Oct 2023 11:47:38 -0400 Subject: [PATCH 11/14] refactor(coro): deprecate dpp::job handler & push dpp::task instead, and remove static_assert in dpp::job (#958) --- .../example_code/coro_awaiting_events.cpp | 4 +- .../example_code/coro_expiring_buttons.cpp | 4 +- docpages/example_code/coro_intro.cpp | 3 +- .../example_code/coro_simple_commands1.cpp | 4 +- .../example_code/coro_simple_commands2.cpp | 4 +- .../using_coroutines/coro_introduction.md | 2 +- include/dpp/coro.h | 3 - include/dpp/coro/async.h | 14 +- include/dpp/coro/coroutine.h | 15 +- include/dpp/coro/job.h | 40 ++--- include/dpp/coro/task.h | 15 +- include/dpp/event_router.h | 166 +++++++++++++++--- include/dpp/export.h | 15 +- include/dpp/utility.h | 5 + src/unittest/coro.cpp | 2 +- 15 files changed, 223 insertions(+), 73 deletions(-) diff --git a/docpages/example_code/coro_awaiting_events.cpp b/docpages/example_code/coro_awaiting_events.cpp index 3f6f63cd19..f5be22b681 100644 --- a/docpages/example_code/coro_awaiting_events.cpp +++ b/docpages/example_code/coro_awaiting_events.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "test") { // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it dpp::message m{"Test"}; @@ -32,7 +32,7 @@ int main() { } }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; diff --git a/docpages/example_code/coro_expiring_buttons.cpp b/docpages/example_code/coro_expiring_buttons.cpp index bc23678576..ed58bdadad 100644 --- a/docpages/example_code/coro_expiring_buttons.cpp +++ b/docpages/example_code/coro_expiring_buttons.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "test") { // Make a message and add a button with its custom ID set to the command interaction's ID so we can identify it dpp::message m{"Test"}; @@ -38,7 +38,7 @@ int main() { } }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command{"test", "Test awaiting for an event", bot.me.id}; diff --git a/docpages/example_code/coro_intro.cpp b/docpages/example_code/coro_intro.cpp index d01588e1e8..c48128547e 100644 --- a/docpages/example_code/coro_intro.cpp +++ b/docpages/example_code/coro_intro.cpp @@ -6,8 +6,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); /* The event is fired when someone issues your commands */ - /* Make note of passing the event by value, this is important (explained below) */ - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "file") { /* Request the image from the URL specified and co_await the response */ dpp::http_request_completion_t result = co_await event.from->creator->co_request("https://dpp.dev/DPP-Logo.png", dpp::m_get); diff --git a/docpages/example_code/coro_simple_commands1.cpp b/docpages/example_code/coro_simple_commands1.cpp index 9c14b916af..09891ea125 100644 --- a/docpages/example_code/coro_simple_commands1.cpp +++ b/docpages/example_code/coro_simple_commands1.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "addemoji") { dpp::cluster *cluster = event.from->creator; // Retrieve parameter values @@ -48,7 +48,7 @@ int main() { } }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command("addemoji", "Add an emoji", bot.me.id); // Add file and name as required parameters diff --git a/docpages/example_code/coro_simple_commands2.cpp b/docpages/example_code/coro_simple_commands2.cpp index 96855f1b3b..ac4683eb45 100644 --- a/docpages/example_code/coro_simple_commands2.cpp +++ b/docpages/example_code/coro_simple_commands2.cpp @@ -5,7 +5,7 @@ int main() { bot.on_log(dpp::utility::cout_logger()); - bot.on_slashcommand([](dpp::slashcommand_t event) -> dpp::job { + bot.on_slashcommand([](const dpp::slashcommand_t& event) -> dpp::task { if (event.command.get_command_name() == "avatar") { // Make a nested coroutine to fetch the guild member requested, that returns it as an optional constexpr auto resolve_member = [](const dpp::slashcommand_t &event) -> dpp::task> { @@ -70,7 +70,7 @@ int main() { }); - bot.on_ready([&bot](const dpp::ready_t & event) { + bot.on_ready([&bot](const dpp::ready_t& event) { if (dpp::run_once()) { dpp::slashcommand command("avatar", "Get your or another user's avatar image", bot.me.id); command.add_option(dpp::command_option(dpp::co_user, "user", "User to fetch the avatar from")); diff --git a/docpages/example_programs/using_coroutines/coro_introduction.md b/docpages/example_programs/using_coroutines/coro_introduction.md index 55c5e123c2..e91a4a271e 100644 --- a/docpages/example_programs/using_coroutines/coro_introduction.md +++ b/docpages/example_programs/using_coroutines/coro_introduction.md @@ -9,7 +9,7 @@ Let's revisit \ref attach-file "attaching a downloaded file", but this time with Coroutines can make commands simpler by eliminating callbacks, which can be very handy in the case of complex commands that rely on a lot of different data or steps. -In order to be a coroutine, a function has to return a special type with special functions; D++ offers dpp::job, dpp::task, and dpp::coroutine, which are designed to work seamlessly with asynchronous calls through dpp::async, which all the functions starting with `co_` such as dpp::cluster::co_message_create return. Event routers can have a dpp::job attached to them, as this object allows to create coroutines that can execute on their own, asynchronously. More on that and the difference between it and the other two types later. To turn a function into a coroutine, simply make it return dpp::job as seen in the example at line 10, then use `co_await` on awaitable types or `co_return`. The moment the execution encounters one of these two keywords, the function is transformed into a coroutine. Coroutines that use dpp::job can be used for event handlers, they can be attached to an event router just the same way as regular event handlers. +In order to be a coroutine, a function has to return a special type with special functions; D++ offers dpp::job, dpp::task, and dpp::coroutine, which are designed to work seamlessly with asynchronous calls through dpp::async, which all the functions starting with `co_` such as dpp::cluster::co_message_create return. Event routers can have a dpp::task coroutine attached to them, as this object allows to create coroutines that can execute on their own, asynchronously. More on that and the difference between it and the other two types later. To turn a function into a coroutine, simply make it return dpp::task as seen in the example at line 10, then use `co_await` on awaitable types or `co_return`. The moment the execution encounters one of these two keywords, the function is transformed into a coroutine. Coroutines that use dpp::task can be used for event handlers, they can be attached to an event router just the same way as regular event handlers. When using a `co_*` function such as `co_message_create`, the request is sent immediately and the returned dpp::async can be `co_await`-ed, at which point the coroutine suspends (pauses) and returns back to its caller; in other words, the program is free to go and do other things while the data is being retrieved and D++ will resume your coroutine when it has the data you need, which will be returned from the `co_await` expression. diff --git a/include/dpp/coro.h b/include/dpp/coro.h index a515d0e53c..e1670e1acf 100644 --- a/include/dpp/coro.h +++ b/include/dpp/coro.h @@ -19,7 +19,6 @@ * ************************************************************************************/ -#ifdef DPP_CORO #pragma once #include "coro/async.h" @@ -27,5 +26,3 @@ #include "coro/job.h" #include "coro/task.h" #include "coro/when_any.h" - -#endif /* DPP_CORO */ diff --git a/include/dpp/coro/async.h b/include/dpp/coro/async.h index 2790aa596b..bd78369dd9 100644 --- a/include/dpp/coro/async.h +++ b/include/dpp/coro/async.h @@ -18,9 +18,19 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct async_dummy { + int* dummy_shared_state = nullptr; +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -490,6 +500,8 @@ class async : private detail::async::async_base { } }; +DPP_CHECK_ABI_COMPAT(async<>, async_dummy); + } // namespace dpp #endif /* DPP_CORO */ diff --git a/include/dpp/coro/coroutine.h b/include/dpp/coro/coroutine.h index 38b80b9127..ea656f578e 100644 --- a/include/dpp/coro/coroutine.h +++ b/include/dpp/coro/coroutine.h @@ -18,9 +18,19 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct coroutine_dummy { + int *handle_dummy = nullptr; +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -562,6 +572,9 @@ inline void coroutine::await_resume_impl() const { } #endif /* _DOXYGEN_ */ +DPP_CHECK_ABI_COMPAT(coroutine, coroutine_dummy) +DPP_CHECK_ABI_COMPAT(coroutine, coroutine_dummy) + } // namespace dpp /** diff --git a/include/dpp/coro/job.h b/include/dpp/coro/job.h index e2473f39c7..f7371977d2 100644 --- a/include/dpp/coro/job.h +++ b/include/dpp/coro/job.h @@ -18,9 +18,18 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct job_dummy { +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -49,15 +58,6 @@ namespace detail { namespace job { -template -inline constexpr bool coroutine_has_no_ref_params_v = false; - -template <> -inline constexpr bool coroutine_has_no_ref_params_v<> = true; - -template -inline constexpr bool coroutine_has_no_ref_params_v = (std::is_invocable_v || !std::is_reference_v) && (!std::is_reference_v && ... && true); - #ifdef DPP_CORO_TEST struct promise{}; #endif @@ -118,31 +118,13 @@ struct promise { * @brief Function called when the job returns. Does nothing. */ void return_void() const noexcept {} - - /** - * @brief Function that will wrap every co_await inside of the job. - */ - template - T await_transform(T &&expr) const noexcept { - /** - * `job` is extremely efficient as a coroutine but this comes with drawbacks : - * It cannot be co_awaited, which means the second it co_awaits something, the program jumps back to the calling function, which continues executing. - * At this point, if the function returns, every object declared in the function including its parameters are destroyed, which causes dangling references. - * This is exactly the same problem as references in lambdas : https://dpp.dev/lambdas-and-locals.html. - * - * If you must pass a reference, pass it as a pointer or with std::ref, but you must fully understand the reason behind this warning, and what to avoid. - * If you prefer a safer type, use `coroutine` for synchronous execution, or `task` for parallel tasks, and co_await them. - */ - static_assert(coroutine_has_no_ref_params_v, "co_await is disabled in dpp::job when taking parameters by reference. read comment above this line for more info"); - - return std::forward(expr); - } }; } // namespace job } // namespace detail +DPP_CHECK_ABI_COMPAT(job, job_dummy) } // namespace dpp /** diff --git a/include/dpp/coro/task.h b/include/dpp/coro/task.h index 5960e0b724..c208440de5 100644 --- a/include/dpp/coro/task.h +++ b/include/dpp/coro/task.h @@ -18,9 +18,19 @@ * limitations under the License. * ************************************************************************************/ +#pragma once + +#include + +namespace dpp { + +struct task_dummy { + int* handle_dummy = nullptr; +}; + +} #ifdef DPP_CORO -#pragma once #include "coro.h" @@ -747,6 +757,9 @@ inline void task::await_resume_impl() const { } #endif /* _DOXYGEN_ */ +DPP_CHECK_ABI_COMPAT(task, task_dummy) +DPP_CHECK_ABI_COMPAT(task, task_dummy) + } // namespace dpp /** diff --git a/include/dpp/event_router.h b/include/dpp/event_router.h index f759570465..027f25a0fc 100644 --- a/include/dpp/event_router.h +++ b/include/dpp/event_router.h @@ -35,6 +35,7 @@ #include #include #include +#include namespace dpp { @@ -200,10 +201,28 @@ template class event_router_t { private: friend class cluster; + /** @brief Non-coro event handler type */ + using regular_handler_t = std::function; + + /** @brief Type that event handlers will be stored as with DPP_CORO off. This is the ABI DPP_CORO has to match */ + using event_handler_abi_t = std::variant>; + #ifdef DPP_CORO friend class detail::event_router::awaitable; + + /** @brief dpp::task coro event handler */ + using task_handler_t = std::function(const T&)>; + + /** @brief Type that event handlers are stored as */ + using event_handler_t = std::variant; + + DPP_CHECK_ABI_COMPAT(event_handler_t, event_handler_abi_t) +#else + /** @brief Type that event handlers are stored as */ + using event_handler_t = event_handler_abi_t; #endif + /** @brief Identifier for the next event handler, will be given to the user on attaching a handler */ event_handle next_handle = 1; /** @@ -217,7 +236,7 @@ template class event_router_t { * be called in they order they are bound to the event * as std::map is an ordered container. */ - std::map> dispatch_container; + std::map dispatch_container; #ifdef DPP_CORO /** @@ -263,7 +282,57 @@ template class event_router_t { warning = warning_function; } + /** + * @brief Handle an event. This function should only be used without coro enabled, otherwise use handle_coro. + */ + void handle(const T& event) const { + if (warning) { + warning(event); + } + + std::shared_lock l(mutex); + for (const auto& [_, listener] : dispatch_container) { + if (!event.is_cancelled()) { + if (std::holds_alternative(listener)) { + std::get(listener)(event); + } else { + throw dpp::logic_exception("cannot handle a coroutine event handler with a library built without DPP_CORO"); + } + } + }; + } + #ifdef DPP_CORO + /** + * @brief Handle an event as a coroutine, ensuring the lifetime of the event object. + */ + dpp::job handle_coro(T event) const { + if (warning) { + warning(event); + } + + resume_awaiters(event); + + std::vector> tasks; + { + std::shared_lock l(mutex); + + for (const auto& [_, listener] : dispatch_container) { + if (!event.is_cancelled()) { + if (std::holds_alternative(listener)) { + tasks.push_back(std::get(listener)(event)); + } else if (std::holds_alternative(listener)) { + std::get(listener)(event); + } + } + }; + } + + for (dpp::task& t : tasks) { + co_await t; // keep the event object alive until all tasks finished + } + } + /** * @brief Attach a suspended coroutine to this event router via detail::event_router::awaitable. * It will be resumed and detached when an event satisfying its condition completes, or it is cancelled. @@ -371,20 +440,25 @@ template class event_router_t { * @param event Class to pass as parameter to all listeners. */ void call(const T& event) const { - if (warning) { - warning(event); - } - #ifdef DPP_CORO - resume_awaiters(event); + handle_coro(event); +#else + handle(event); #endif + }; - std::shared_lock l(mutex); - for (const auto& [_, listener] : dispatch_container) { - if (!event.is_cancelled()) { - listener(event); - } - }; + /** + * @brief Call all attached listeners. + * Listeners may cancel, by calling the event.cancel method. + * + * @param event Class to pass as parameter to all listeners. + */ + void call(T&& event) const { +#ifdef DPP_CORO + handle_coro(std::move(event)); +#else + handle(std::move(event)); +#endif }; #ifdef DPP_CORO @@ -486,8 +560,8 @@ template class event_router_t { #ifdef _DOXYGEN_ /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)` (the latter requires DPP_CORO to be defined), + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)` (the latter requires DPP_CORO to be defined), * where T is the event type for this event router. * * This has the exact same behavior as using \ref attach(F&&) "attach". @@ -502,8 +576,8 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)` (the latter requires DPP_CORO to be defined), + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)` (the latter requires DPP_CORO to be defined), * where T is the event type for this event router. * * @param fun Callable to attach to event @@ -516,40 +590,82 @@ template class event_router_t { # ifdef DPP_CORO /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)`, where T is the event type for this event router. + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)`, where T is the event type for this event router. * * @param fun Callable to attach to event * @return event_handle An event handle unique to this event, used to * detach the listener from the event later if necessary. */ template - requires (utility::callable_returns || utility::callable_returns) + requires (utility::callable_returns || utility::callable_returns, const T&> || utility::callable_returns) [[maybe_unused]] event_handle operator()(F&& fun) { return this->attach(std::forward(fun)); } /** * @brief Attach a callable to the event, adding a listener. - * The callable should either be of the form `void(const T &)` or - * `dpp::job(T)`, where T is the event type for this event router. + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)`, where T is the event type for this event router. * * @param fun Callable to attach to event * @return event_handle An event handle unique to this event, used to * detach the listener from the event later if necessary. */ template - requires (utility::callable_returns || utility::callable_returns) + requires (utility::callable_returns) [[maybe_unused]] event_handle attach(F&& fun) { std::unique_lock l(mutex); event_handle h = next_handle++; - dispatch_container.emplace(h, std::forward(fun)); + dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); + return h; + } + + /** + * @brief Attach a callable to the event, adding a listener. + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)`, where T is the event type for this event router. + * + * @param fun Callable to attach to event + * @return event_handle An event handle unique to this event, used to + * detach the listener from the event later if necessary. + */ + template + requires (utility::callable_returns, const T&>) + [[maybe_unused]] event_handle attach(F&& fun) { + assert(dpp::utility::is_coro_enabled()); + + std::unique_lock l(mutex); + event_handle h = next_handle++; + dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); + return h; + } + + /** + * @brief Attach a callable to the event, adding a listener. + * The callable should either be of the form `void(const T&)` or + * `dpp::task(const T&)`, where T is the event type for this event router. + * + * @deprecated dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task instead. + * @param fun Callable to attach to event + * @return event_handle An event handle unique to this event, used to + * detach the listener from the event later if necessary. + */ + template + requires (utility::callable_returns) + DPP_DEPRECATED("dpp::job event handlers are deprecated and will be removed in a future version, use dpp::task instead") + [[maybe_unused]] event_handle attach(F&& fun) { + assert(dpp::utility::is_coro_enabled()); + + std::unique_lock l(mutex); + event_handle h = next_handle++; + dispatch_container.emplace(std::piecewise_construct, std::forward_as_tuple(h), std::forward_as_tuple(std::in_place_type_t{}, std::forward(fun))); return h; } # else /** * @brief Attach a callable to the event, adding a listener. - * The callable should be of the form `void(const T &)` + * The callable should be of the form `void(const T&)` * where T is the event type for this event router. * * @param fun Callable to attach to event @@ -563,7 +679,7 @@ template class event_router_t { /** * @brief Attach a callable to the event, adding a listener. - * The callable should be of the form `void(const T &)` + * The callable should be of the form `void(const T&)` * where T is the event type for this event router. * * @warning You cannot call this within an event handler. diff --git a/include/dpp/export.h b/include/dpp/export.h index 4672f2f78d..c8ba8f6f69 100644 --- a/include/dpp/export.h +++ b/include/dpp/export.h @@ -65,4 +65,17 @@ #define NOMINMAX #include -#endif \ No newline at end of file +#endif + +#ifdef _DOXYGEN_ + /** @brief Macro that expands to [[deprecated(reason)]] when including the library, nothing when building the library */ + #define DPP_DEPRECATED(reason) +#else /* !_DOXYGEN_ */ + #if defined(DPP_BUILD) || defined(DPP_NO_DEPRECATED) + /** @brief Macro that expands to [[deprecated(reason)]] when including the library, nothing when building the library */ + #define DPP_DEPRECATED(reason) + #else + /** @brief Macro that expands to [[deprecated(reason)]] when including the library, nothing when building the library */ + #define DPP_DEPRECATED(reason) [[deprecated(reason)]] + #endif +#endif /* _DOXYGEN_ */ diff --git a/include/dpp/utility.h b/include/dpp/utility.h index ed954058fa..1410b8599e 100644 --- a/include/dpp/utility.h +++ b/include/dpp/utility.h @@ -37,6 +37,11 @@ namespace dpp { enum sticker_format : uint8_t; +/** @brief Macro that expands to static_asserts checking sizeof and alignof are equal between two types */ +#define DPP_CHECK_ABI_COMPAT(a, b) \ +static_assert(sizeof(a) == sizeof(b), #a " and " #b " must be the same size for ABI compatibility"); \ +static_assert(alignof(a) == alignof(b), #a " and " #b " must be the same alignment for ABI compatibility"); \ + /** * @brief Utility helper functions, generally for logging, running programs, time/date manipulation, etc */ diff --git a/src/unittest/coro.cpp b/src/unittest/coro.cpp index 1e6e819cf0..c5047a134c 100644 --- a/src/unittest/coro.cpp +++ b/src/unittest/coro.cpp @@ -410,7 +410,7 @@ void coro_offline_tests() } void event_handler_test(dpp::cluster *bot) { - bot->on_message_create([](dpp::message_create_t event) -> dpp::job { + bot->on_message_create([](dpp::message_create_t event) -> dpp::task { if (event.msg.content == "coro test") { dpp::cluster *bot = event.from->creator; From a6cd935b55486ba208a1145f95ff4c6198bdcb70 Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Tue, 24 Oct 2023 16:54:46 +0100 Subject: [PATCH 12/14] feat: removed version_checker for UE. (#970) --- include/dpp/restresults.h | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/include/dpp/restresults.h b/include/dpp/restresults.h index 55170c24a4..9033241c5d 100644 --- a/include/dpp/restresults.h +++ b/include/dpp/restresults.h @@ -66,7 +66,14 @@ struct DPP_EXPORT version_checker { } }; +/* + * We need to tell DPP to NOT do the version checker if something from Unreal Engine is defined. + * We have to do this because UE is causing some weirdness where the version checker is broken and always errors. + * This is really only for DPP-UE. There is no reason to not do the version checker unless you are in Unreal Engine. + */ +#if !defined(UE_BUILD_DEBUG) && !defined(UE_BUILD_DEVELOPMENT) && !defined(UE_BUILD_TEST) && !defined(UE_BUILD_SHIPPING) && !defined(UE_GAME) && !defined(UE_EDITOR) && !defined(UE_BUILD_SHIPPING_WITH_EDITOR) && !defined(UE_BUILD_DOCS) static version_checker dpp_vc; +#endif /** From 3eb549dd2cb4de10a1df5dfb1b51850ada5b308f Mon Sep 17 00:00:00 2001 From: Archie Jaskowicz Date: Wed, 25 Oct 2023 02:12:11 +0100 Subject: [PATCH 13/14] feat: added support for Premium App Subscriptions (#969) --- .cspell.json | 4 +- include/dpp/appcommand.h | 165 +++++++++++++++++++++----- include/dpp/cluster.h | 81 +++++++++++++ include/dpp/cluster_coro_calls.h | 55 +++++++++ include/dpp/cluster_sync_calls.h | 67 +++++++++++ include/dpp/dispatcher.h | 34 ++++++ include/dpp/entitlement.h | 160 +++++++++++++++++++++++++ include/dpp/event.h | 5 + include/dpp/restresults.h | 8 +- include/dpp/sku.h | 159 +++++++++++++++++++++++++ src/dpp/cluster/entitlement.cpp | 80 +++++++++++++ src/dpp/cluster/sku.cpp | 30 +++++ src/dpp/cluster_coro_calls.cpp | 16 +++ src/dpp/cluster_sync_calls.cpp | 16 +++ src/dpp/discordevents.cpp | 3 + src/dpp/entitlement.cpp | 78 ++++++++++++ src/dpp/events/entitlement_create.cpp | 47 ++++++++ src/dpp/events/entitlement_delete.cpp | 47 ++++++++ src/dpp/events/entitlement_update.cpp | 47 ++++++++ src/dpp/sku.cpp | 89 ++++++++++++++ src/dpp/slashcommand.cpp | 10 +- 21 files changed, 1170 insertions(+), 31 deletions(-) create mode 100644 include/dpp/entitlement.h create mode 100644 include/dpp/sku.h create mode 100644 src/dpp/cluster/entitlement.cpp create mode 100644 src/dpp/cluster/sku.cpp create mode 100644 src/dpp/entitlement.cpp create mode 100644 src/dpp/events/entitlement_create.cpp create mode 100644 src/dpp/events/entitlement_delete.cpp create mode 100644 src/dpp/events/entitlement_update.cpp create mode 100644 src/dpp/sku.cpp diff --git a/.cspell.json b/.cspell.json index 459a14315c..8b263fcb2a 100644 --- a/.cspell.json +++ b/.cspell.json @@ -133,7 +133,9 @@ "cvtps", "neww", "STDCORO", - "NOMINMAX" + "NOMINMAX", + "sku", + "skus" ], "flagWords": [ "hte" diff --git a/include/dpp/appcommand.h b/include/dpp/appcommand.h index 10d49456dc..5ffc850525 100644 --- a/include/dpp/appcommand.h +++ b/include/dpp/appcommand.h @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -297,13 +298,51 @@ void to_json(nlohmann::json& j, const command_option& opt); * @brief Response types when responding to an interaction within on_interaction_create. */ enum interaction_response_type { - ir_pong = 1, //!< Acknowledge a Ping - ir_channel_message_with_source = 4, //!< respond to an interaction with a message - ir_deferred_channel_message_with_source = 5, //!< Acknowledge an interaction and edit a response later, the user sees a loading state - ir_deferred_update_message = 6, //!< for components, acknowledge an interaction and edit the original message later; the user does not see a loading state - ir_update_message = 7, //!< for components, edit the message the component was attached to - ir_autocomplete_reply = 8, //!< Reply to autocomplete interaction. Be sure to do this within 500ms of the interaction! - ir_modal_dialog = 9, //!< A modal dialog box + /** + * @brief Acknowledge a Ping + */ + ir_pong = 1, + /** + * @brief Respond to an interaction with a message. + */ + ir_channel_message_with_source = 4, + /** + * @brief Acknowledge an interaction and edit a response later, the user sees a loading state + */ + ir_deferred_channel_message_with_source = 5, + + /** + * @brief For components, acknowledge an interaction and edit the original message later; the user does not see a loading state. + */ + ir_deferred_update_message = 6, + + /** + * @brief For components, edit the message the component was attached to. + */ + ir_update_message = 7, + + /** + * @brief Reply to autocomplete interaction. + * + * @note Be sure to do this within 500ms of the interaction! + */ + ir_autocomplete_reply = 8, + + /** + * @brief A modal dialog box + * + * @note Not available for modal submit and ping interactions. + */ + ir_modal_dialog = 9, + + /** + * @brief Acknowledge a interaction with an upgrade button, only available for apps with monetization enabled. + * + * @see https://discord.com/developers/docs/monetization/entitlements#premiumrequired-interaction-response + * @note Not available for autocomplete and ping interactions. + * @warning This response does not support using `content`, `embeds`, or `attachments`, so reply with no data when using this! + */ + ir_premium_required = 10, }; /** @@ -732,26 +771,98 @@ class DPP_EXPORT interaction : public managed, public json_interface data; //!< Optional: the command data payload - snowflake guild_id; //!< Optional: the guild it was sent from - snowflake channel_id; //!< Optional: the channel it was sent from - dpp::channel channel; //!< Optional: The partial channel object where it was sent from - snowflake message_id; //!< Originating message id for context menu actions - permission app_permissions; //!< Permissions of the bot in the channel/guild where this command was issued - message msg; //!< Originating message for context menu actions - guild_member member; //!< Optional: guild member data for the invoking user, including permissions. Filled when the interaction is invoked in a guild - user usr; //!< User object for the invoking user - std::string token; //!< a continuation token for responding to the interaction - uint8_t version; //!< read-only property, always 1 - command_resolved resolved; //!< Resolved data e.g. users, members, roles, channels, permissions, etc. - std::string locale; //!< User's [locale](https://discord.com/developers/docs/reference#locales) (language) - std::string guild_locale; //!< Guild's locale (language) - for guild interactions only - cache_policy_t cache_policy; //!< Cache policy from cluster - - /** - * @brief Construct a new interaction object + /** + * @brief ID of the application this interaction is for. + */ + snowflake application_id; + + /** + * @brief The type of interaction from dpp::interaction_type. + */ + uint8_t type; + + /** + * @brief Optional: the command data payload. + */ + std::variant data; + + /** + * @brief Optional: the guild it was sent from. + */ + snowflake guild_id; + + /** + * @brief Optional: the channel it was sent from + */ + snowflake channel_id; + + /** + * @brief Optional: The partial channel object where it was sent from. + */ + dpp::channel channel; + + /** + * @brief Originating message id for context menu actions. + */ + snowflake message_id; + + /** + * @brief Permissions of the bot in the channel/guild where this command was issued. + */ + permission app_permissions; + + /** + * @brief Originating message for context menu actions. + */ + message msg; + + /** + * @brief Optional: guild member data for the invoking user, including permissions. Filled when the interaction is invoked in a guild + */ + guild_member member; + + /** + * @brief User object for the invoking user. + */ + user usr; + + /** + * @brief A continuation token for responding to the interaction. + */ + std::string token; + + /** + * @brief Read-only property, always 1. + */ + uint8_t version; + + /** + * @brief Resolved data e.g. users, members, roles, channels, permissions, etc. + */ + command_resolved resolved; + + /** + * @brief User's [locale](https://discord.com/developers/docs/reference#locales) (language). + */ + std::string locale; + + /** + * @brief Guild's locale (language) - for guild interactions only. + */ + std::string guild_locale; + + /** + * @brief Cache policy from cluster. + */ + cache_policy_t cache_policy; + + /** + * @brief For monetized apps, any entitlements for the invoking user, representing access to premium SKUs. + */ + std::vector entitlements; + + /** + * @brief Construct a new interaction object. */ interaction(); diff --git a/include/dpp/cluster.h b/include/dpp/cluster.h index 219e60800b..d74a69472a 100644 --- a/include/dpp/cluster.h +++ b/include/dpp/cluster.h @@ -1312,6 +1312,36 @@ class DPP_EXPORT cluster { */ event_router_t on_stage_instance_delete; + /** + * @brief Called when a user subscribes to an SKU. + * + * @see https://discord.com/developers/docs/monetization/entitlements#new-entitlement + * @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID. + * The function signature for this event takes a single `const` reference of type channel_delete_t&, and returns void. + */ + event_router_t on_entitlement_create; + + + /** + * @brief Called when a user's subscription renews for the next billing period. + * The `ends_at` field will have an updated value with the new expiration date. + * + * @see https://discord.com/developers/docs/monetization/entitlements#updated-entitlement + * @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID. + * The function signature for this event takes a single `const` reference of type channel_update_t&, and returns void. + */ + event_router_t on_entitlement_update; + + /** + * @brief Called when a user's entitlement is deleted. + * These events are infrequent and only occur if Discord issues a refund, or Discord removes an entitlement via "internal tooling". + * Entitlements **are not deleted** when they expire. + * + * @see https://discord.com/developers/docs/monetization/entitlements#deleted-entitlement + * @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID. + * The function signature for this event takes a single `const` reference of type channel_update_t&, and returns void. + */ + event_router_t on_entitlement_delete; /** * @brief Post a REST request. Where possible use a helper method instead like message_create @@ -3624,6 +3654,57 @@ class DPP_EXPORT cluster { */ void automod_rule_delete(snowflake guild_id, snowflake rule_id, command_completion_event_t callback = utility::log_error()); + /** + * @brief Returns all entitlements for a given app, active and expired. + * + * @see https://discord.com/developers/docs/monetization/entitlements#list-entitlements + * @param user_id User ID to look up entitlements for. + * @param sku_ids List of SKU IDs to check entitlements for. + * @param before_id Retrieve entitlements before this entitlement ID. + * @param after_id Retrieve entitlements after this entitlement ID. + * @param limit Number of entitlements to return, 1-100 (default 100). + * @param guild_id Guild ID to look up entitlements for. + * @param exclude_ended Whether ended entitlements should be excluded from the search. + * @param callback Function to call when the API call completes. + * On success the callback will contain a dpp::emoji_map object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void entitlements_get(snowflake user_id = 0, const std::vector& sku_ids = {}, snowflake before_id = 0, snowflake after_id = 0, uint8_t limit = 100, snowflake guild_id = 0, bool exclude_ended = false, command_completion_event_t callback = utility::log_error()); + + /** + * @brief Creates a test entitlement to a given SKU for a given guild or user. + * Discord will act as though that user or guild has entitlement to your premium offering. + * + * @see https://discord.com/developers/docs/monetization/entitlements#create-test-entitlement + * @param new_entitlement The entitlement to create. + * Make sure your dpp::entitlement_type (inside your dpp::entitlement object) matches the type of the owner_id + * (if type is guild, owner_id is a guild id), otherwise it won't work! + * @param callback Function to call when the API call completes. + * On success the callback will contain a dpp::entitlement object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void entitlement_test_create(const class entitlement& new_entitlement, command_completion_event_t callback = utility::log_error()); + + /** + * @brief Deletes a currently-active test entitlement. + * Discord will act as though that user or guild no longer has entitlement to your premium offering. + * + * @see https://discord.com/developers/docs/monetization/entitlements#delete-test-entitlement + * @param entitlement_id The test entitlement to delete. + * @param callback Function to call when the API call completes. + * On success the callback will contain a dpp::confirmation object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void entitlement_test_delete(snowflake entitlement_id, command_completion_event_t callback = utility::log_error()); + + /** + * @brief Returns all SKUs for a given application. + * @note Because of how Discord's SKU and subscription systems work, you will see two SKUs for your premium offering. + * For integration and testing entitlements, you should use the SKU with type: 5. + * + * @see https://discord.com/developers/docs/monetization/skus#list-skus + * @param callback Function to call when the API call completes. + * On success the callback will contain a dpp::confirmation object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error(). + */ + void skus_get(command_completion_event_t callback = utility::log_error()); + #include #ifdef DPP_CORO #include diff --git a/include/dpp/cluster_coro_calls.h b/include/dpp/cluster_coro_calls.h index ddbb9a2395..22cdcef0c4 100644 --- a/include/dpp/cluster_coro_calls.h +++ b/include/dpp/cluster_coro_calls.h @@ -696,6 +696,49 @@ */ [[nodiscard]] async co_guild_emojis_get(snowflake guild_id); +/** + * @brief Returns all entitlements for a given app, active and expired. + * + * @see dpp::cluster::entitlements_get + * @see https://discord.com/developers/docs/monetization/entitlements#list-entitlements + * @param user_id User ID to look up entitlements for. + * @param sku_ids List of SKU IDs to check entitlements for. + * @param before_id Retrieve entitlements before this entitlement ID. + * @param after_id Retrieve entitlements after this entitlement ID. + * @param limit Number of entitlements to return, 1-100 (default 100). + * @param guild_id Guild ID to look up entitlements for. + * @param exclude_ended Whether ended entitlements should be excluded from the search. + * @return entitlement_map returned object on completion + * \memberof dpp::cluster + */ +[[nodiscard]] async co_entitlements_get(snowflake user_id = 0, const std::vector& sku_ids = {}, snowflake before_id = 0, snowflake after_id = 0, uint8_t limit = 100, snowflake guild_id = 0, bool exclude_ended = false); + +/** + * @brief Creates a test entitlement to a given SKU for a given guild or user. + * Discord will act as though that user or guild has entitlement to your premium offering. + * + * @see dpp::cluster::entitlement_test_create + * @see https://discord.com/developers/docs/monetization/entitlements#create-test-entitlement + * @param new_entitlement The entitlement to create. + * Make sure your dpp::entitlement_type (inside your dpp::entitlement object) matches the type of the owner_id + * (if type is guild, owner_id is a guild id), otherwise it won't work! + * @return entitlement returned object on completion + * \memberof dpp::cluster + */ +[[nodiscard]] async co_entitlement_test_create(const class entitlement& new_entitlement); + +/** + * @brief Deletes a currently-active test entitlement. + * Discord will act as though that user or guild no longer has entitlement to your premium offering. + * + * @see dpp::cluster::entitlement_test_delete + * @see https://discord.com/developers/docs/monetization/entitlements#delete-test-entitlement + * @param entitlement_id The test entitlement to delete. + * @return confirmation returned object on completion + * \memberof dpp::cluster + */ +[[nodiscard]] async co_entitlement_test_delete(snowflake entitlement_id); + /** * @brief Get the gateway information for the bot using the token * @see dpp::cluster::get_gateway_bot @@ -1727,6 +1770,18 @@ */ [[nodiscard]] async co_guild_event_get(snowflake guild_id, snowflake event_id); +/** + * @brief Returns all SKUs for a given application. + * @note Because of how Discord's SKU and subscription systems work, you will see two SKUs for your premium offering. + * For integration and testing entitlements, you should use the SKU with type: 5. + * + * @see dpp::cluster::skus_get + * @see https://discord.com/developers/docs/monetization/skus#list-skus + * @return sku_map returned object on completion + * \memberof dpp::cluster + */ +[[nodiscard]] async co_skus_get(); + [[nodiscard]] async co_stage_instance_create(const stage_instance& si); diff --git a/include/dpp/cluster_sync_calls.h b/include/dpp/cluster_sync_calls.h index 1594d8e0a4..26e81f111a 100644 --- a/include/dpp/cluster_sync_calls.h +++ b/include/dpp/cluster_sync_calls.h @@ -864,6 +864,58 @@ emoji guild_emoji_get_sync(snowflake guild_id, snowflake emoji_id); */ emoji_map guild_emojis_get_sync(snowflake guild_id); +/** + * @brief Returns all entitlements for a given app, active and expired. + * + * @see dpp::cluster::entitlements_get + * @see https://discord.com/developers/docs/monetization/entitlements#list-entitlements + * @param user_id User ID to look up entitlements for. + * @param sku_ids List of SKU IDs to check entitlements for. + * @param before_id Retrieve entitlements before this entitlement ID. + * @param after_id Retrieve entitlements after this entitlement ID. + * @param limit Number of entitlements to return, 1-100 (default 100). + * @param guild_id Guild ID to look up entitlements for. + * @param exclude_ended Whether ended entitlements should be excluded from the search. + * @return entitlement_map returned object on completion + * \memberof dpp::cluster + * @throw dpp::rest_exception upon failure to execute REST function + * @warning This function is a blocking (synchronous) call and should only be used from within a separate thread. + * Avoid direct use of this function inside an event handler. + */ +entitlement_map entitlements_get_sync(snowflake user_id = 0, const std::vector& sku_ids = {}, snowflake before_id = 0, snowflake after_id = 0, uint8_t limit = 100, snowflake guild_id = 0, bool exclude_ended = false); + +/** + * @brief Creates a test entitlement to a given SKU for a given guild or user. + * Discord will act as though that user or guild has entitlement to your premium offering. + * + * @see dpp::cluster::entitlement_test_create + * @see https://discord.com/developers/docs/monetization/entitlements#create-test-entitlement + * @param new_entitlement The entitlement to create. + * Make sure your dpp::entitlement_type (inside your dpp::entitlement object) matches the type of the owner_id + * (if type is guild, owner_id is a guild id), otherwise it won't work! + * @return entitlement returned object on completion + * \memberof dpp::cluster + * @throw dpp::rest_exception upon failure to execute REST function + * @warning This function is a blocking (synchronous) call and should only be used from within a separate thread. + * Avoid direct use of this function inside an event handler. + */ +entitlement entitlement_test_create_sync(const class entitlement& new_entitlement); + +/** + * @brief Deletes a currently-active test entitlement. + * Discord will act as though that user or guild no longer has entitlement to your premium offering. + * + * @see dpp::cluster::entitlement_test_delete + * @see https://discord.com/developers/docs/monetization/entitlements#delete-test-entitlement + * @param entitlement_id The test entitlement to delete. + * @return confirmation returned object on completion + * \memberof dpp::cluster + * @throw dpp::rest_exception upon failure to execute REST function + * @warning This function is a blocking (synchronous) call and should only be used from within a separate thread. + * Avoid direct use of this function inside an event handler. + */ +confirmation entitlement_test_delete_sync(snowflake entitlement_id); + /** * @brief Get the gateway information for the bot using the token * @see dpp::cluster::get_gateway_bot @@ -2117,6 +2169,21 @@ scheduled_event guild_event_edit_sync(const scheduled_event& event); */ scheduled_event guild_event_get_sync(snowflake guild_id, snowflake event_id); +/** + * @brief Returns all SKUs for a given application. + * @note Because of how Discord's SKU and subscription systems work, you will see two SKUs for your premium offering. + * For integration and testing entitlements, you should use the SKU with type: 5. + * + * @see dpp::cluster::skus_get + * @see https://discord.com/developers/docs/monetization/skus#list-skus + * @return sku_map returned object on completion + * \memberof dpp::cluster + * @throw dpp::rest_exception upon failure to execute REST function + * @warning This function is a blocking (synchronous) call and should only be used from within a separate thread. + * Avoid direct use of this function inside an event handler. + */ +sku_map skus_get_sync(); + stage_instance stage_instance_create_sync(const stage_instance& si); diff --git a/include/dpp/dispatcher.h b/include/dpp/dispatcher.h index 5fc92ef4fd..0df6996f42 100644 --- a/include/dpp/dispatcher.h +++ b/include/dpp/dispatcher.h @@ -42,6 +42,7 @@ #include #include #include +#include #include #include #include @@ -1918,5 +1919,38 @@ struct DPP_EXPORT voice_client_disconnect_t : public event_dispatch_t { snowflake user_id = {}; }; +/** @brief Delete stage instance */ +struct DPP_EXPORT entitlement_create_t : public event_dispatch_t { + using event_dispatch_t::event_dispatch_t; + using event_dispatch_t::operator=; + + /** + * @brief The created entitlement. + */ + entitlement created = {}; +}; + +/** @brief Delete stage instance */ +struct DPP_EXPORT entitlement_update_t : public event_dispatch_t { + using event_dispatch_t::event_dispatch_t; + using event_dispatch_t::operator=; + + /** + * @brief The entitlement that was updated. + */ + entitlement updating_entitlement = {}; +}; + +/** @brief Delete stage instance */ +struct DPP_EXPORT entitlement_delete_t : public event_dispatch_t { + using event_dispatch_t::event_dispatch_t; + using event_dispatch_t::operator=; + + /** + * @brief The deleted entitlement. + */ + entitlement deleted = {}; +}; + } // namespace dpp diff --git a/include/dpp/entitlement.h b/include/dpp/entitlement.h new file mode 100644 index 0000000000..e1cfab8818 --- /dev/null +++ b/include/dpp/entitlement.h @@ -0,0 +1,160 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace dpp { + +/** + * @brief The type of entitlement. + * */ +enum entitlement_type : uint8_t { + /** + * @brief A subscription for a guild. + * @warning This can only be used when creating a test entitlement. + */ + GUILD_SUBSCRIPTION = 1, + /** + * @brief A subscription for a user. + * @warning This can only be used when creating a test entitlement. + */ + USER_SUBSCRIPTION = 2, + /** + * @brief Entitlement was purchased as an app subscription. + */ + APPLICATION_SUBSCRIPTION = 8 +}; + +/** + * @brief Entitlement flags. + */ +enum entitlement_flags : uint16_t { + /** + * @brief Entitlement was deleted + */ + ent_deleted = 0b000000000000001, +}; + +/** + * @brief A definition of a discord entitlement. + */ +class DPP_EXPORT entitlement : public managed, public json_interface { +protected: + friend struct json_interface; + + /** Read class values from json object + * @param j A json object to read from + * @return A reference to self + */ + entitlement& fill_from_json_impl(nlohmann::json* j); + + /** + * @brief Build json for this entitlement object + * + * @param with_id include the ID in the json + * @return json JSON object + */ + json to_json_impl(bool with_id = false) const; + +public: + /** + * @brief ID of the SKU + */ + snowflake sku_id{0}; + + /** + * @brief ID of the parent application + */ + snowflake application_id{0}; + + /** + * @brief Optional: ID of the user/guild that is granted access to the entitlement's SKU + */ + snowflake owner_id{0}; + + /** + * @brief The type of entitlement. + */ + entitlement_type type = entitlement_type::APPLICATION_SUBSCRIPTION; + + /** + * @brief Optional: Start date at which the entitlement is valid. + * + * @note Not present when using test entitlements. + */ + time_t starts_at{0}; + + /** + * @brief Optional: Date at which the entitlement is no longer valid. + * + * @note Not present when using test entitlements. + */ + time_t ends_at{0}; + + /** + * @brief Flags bitmap from dpp::entitlement_flags + */ + uint16_t flags{0}; + + /** + * @brief Construct a new entitlement object + */ + entitlement() = default; + + /** + * @brief Construct a new entitlement object with sku_id, ID, application_id, type, and flags. + * + * @param sku_id The ID of the SKU. + * @param id The ID of the entitlement. + * @param application_id The ID of the parent application. + * @param type The type of entitlement (Should only ever be APPLICATION_SUBSCRIPTION unless you going to use this object as a parameter for dpp::cluster::entitlement_test_create). + * @param flags The flags for the SKU from dpp::entitlement_flags. + */ + entitlement(const snowflake sku_id, const snowflake id = 0, const snowflake application_id = 0, const entitlement_type type = dpp::entitlement_type::APPLICATION_SUBSCRIPTION, const uint8_t flags = 0); + + /** + * @brief Get the type of entitlement. + * + * @return entitlement_type Entitlement type + */ + entitlement_type get_type() const; + + /** + * @brief Was the entitlement deleted? + * + * @return true if the entitlement was deleted. + */ + bool is_deleted() const; +}; + +/** + * @brief Group of entitlements. + */ +typedef std::unordered_map entitlement_map; + +} // namespace dpp diff --git a/include/dpp/event.h b/include/dpp/event.h index b0cb96ea08..b9b3f003f7 100644 --- a/include/dpp/event.h +++ b/include/dpp/event.h @@ -148,4 +148,9 @@ event_decl(automod_rule_execute, AUTO_MODERATION_ACTION_EXECUTION); /* Audit log */ event_decl(guild_audit_log_entry_create, GUILD_AUDIT_LOG_ENTRY_CREATE); +/* Entitlements */ +event_decl(entitlement_create, ENTITLEMENT_CREATE); +event_decl(entitlement_update, ENTITLEMENT_UPDATE); +event_decl(entitlement_delete, ENTITLEMENT_DELETE); + } // namespace dpp::events diff --git a/include/dpp/restresults.h b/include/dpp/restresults.h index 9033241c5d..a340b50ebc 100644 --- a/include/dpp/restresults.h +++ b/include/dpp/restresults.h @@ -43,6 +43,8 @@ #include #include #include +#include +#include namespace dpp { @@ -202,7 +204,11 @@ typedef std::variant< automod_rule, automod_rule_map, onboarding, - welcome_screen + welcome_screen, + entitlement, + entitlement_map, + sku, + sku_map > confirmable_t; /** diff --git a/include/dpp/sku.h b/include/dpp/sku.h new file mode 100644 index 0000000000..88b07d0728 --- /dev/null +++ b/include/dpp/sku.h @@ -0,0 +1,159 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ + +#pragma once +#include +#include +#include +#include +#include +#include + +namespace dpp { + +/** + * @brief The type of SKU. + * */ +enum sku_type : uint8_t { + /** + * @brief Represents a recurring subscription + */ + SUBSCRIPTION = 5, + /** + * @brief System-generated group for each SUBSCRIPTION SKU created + * @warning These are automatically created for each subscription SKU and are not used at this time. Please refrain from using these. + */ + SUBSCRIPTION_GROUP = 6, +}; + +/** + * @brief SKU flags. + */ +enum sku_flags : uint16_t { + /** + * @brief SKU is available for purchase + */ + sku_available = 0b000000000000100, + /** + * @brief Recurring SKU that can be purchased by a user and applied to a single server. Grants access to every user in that server. + */ + sku_guild_subscription = 0b000000010000000, + /** + * @brief Recurring SKU purchased by a user for themselves. Grants access to the purchasing user in every server. + */ + sku_user_subscription = 0b000000100000000, +}; + +/** + * @brief A definition of a discord SKU. + */ +class DPP_EXPORT sku : public managed, public json_interface { +protected: + friend struct json_interface; + + /** Read class values from json object + * @param j A json object to read from + * @return A reference to self + */ + sku& fill_from_json_impl(nlohmann::json* j); + + /** + * @brief Build json for this SKU object + * + * @param with_id include the ID in the json + * @return json JSON object + */ + json to_json_impl(bool with_id = false) const; + +public: + /** + * @brief The type of SKU. + */ + sku_type type = sku_type::SUBSCRIPTION; + + /** + * @brief ID of the parent application + */ + snowflake application_id{0}; + + /** + * @brief Customer-facing name of your premium offering + */ + std::string name{}; + + /** + * @brief System-generated URL slug based on the SKU's name + */ + std::string slug{}; + + /** + * @brief Flags bitmap from dpp::sku_flags + */ + uint16_t flags{0}; + + /** + * @brief Construct a new SKU object + */ + sku() = default; + + /** + * @brief Construct a new SKU object with all data required. + * + * @param id SKU id. + */ + sku(const snowflake id, const sku_type type, const snowflake application_id, const std::string name, const std::string slug, const uint16_t flags); + + /** + * @brief Get the type of SKU. + * + * @return sku_type SKU type + */ + sku_type get_type() const; + + /** + * @brief Is the SKU available for purchase? + * + * @return true if the SKU can be purchased. + */ + bool is_available() const; + + /** + * @brief Is the SKU a guild subscription? + * + * @return true if the SKU is a guild subscription. + */ + bool is_guild_subscription() const; + + /** + * @brief Is the SKU a user subscription? + * + * @return true if the SKU is a user subscription + */ + bool is_user_subscription() const; +}; + +/** + * @brief Group of SKUs. + */ +typedef std::unordered_map sku_map; + +} // namespace dpp diff --git a/src/dpp/cluster/entitlement.cpp b/src/dpp/cluster/entitlement.cpp new file mode 100644 index 0000000000..1783e79296 --- /dev/null +++ b/src/dpp/cluster/entitlement.cpp @@ -0,0 +1,80 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include +#include + +namespace dpp { + +void cluster::entitlements_get(snowflake user_id, const std::vector& sku_ids, snowflake before_id, snowflake after_id, uint8_t limit, snowflake guild_id, bool exclude_ended, command_completion_event_t callback) { + json j; + + if(user_id) { + j["user_id"] = user_id.str(); + } + + if(!sku_ids.empty()) { + /* Why can't Discord just be consistent and accept an array of ids???? + * Why just out of nowhere introduce a "comma-delimited set of snowflakes", like what. + * Just allow an array like you normally do!!!!!!!!!!!! + */ + std::string ids = ""; + for(size_t i = 0; i(this, API_PATH "/applications", me.id.str(), "entitlements", m_get, j, callback); +} + +void cluster::entitlement_test_create(const class entitlement& new_entitlement, command_completion_event_t callback) { + json j; + j["sku_id"] = new_entitlement.sku_id.str(); + j["owner_id"] = new_entitlement.owner_id.str(); + j["owner_type"] = new_entitlement.type; + rest_request(this, API_PATH "/applications", me.id.str(), "entitlements", m_post, j, callback); +} + +void cluster::entitlement_test_delete(const class snowflake entitlement_id, command_completion_event_t callback) { + rest_request(this, API_PATH "/applications", me.id.str(), "entitlements/" + entitlement_id.str(), m_delete, "", callback); +} + +} // namespace dpp diff --git a/src/dpp/cluster/sku.cpp b/src/dpp/cluster/sku.cpp new file mode 100644 index 0000000000..20f23f3e77 --- /dev/null +++ b/src/dpp/cluster/sku.cpp @@ -0,0 +1,30 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include +#include + +namespace dpp { + +void cluster::skus_get(command_completion_event_t callback) { + rest_request_list(this, API_PATH "/applications", me.id.str(), "entitlements", m_get, "", callback); +} + +} // namespace dpp diff --git a/src/dpp/cluster_coro_calls.cpp b/src/dpp/cluster_coro_calls.cpp index 528df234ab..d03183cdeb 100644 --- a/src/dpp/cluster_coro_calls.cpp +++ b/src/dpp/cluster_coro_calls.cpp @@ -259,6 +259,18 @@ async cluster::co_guild_emojis_get(snowflake guild_id) return async{ this, static_cast(&cluster::guild_emojis_get), guild_id }; } +async cluster::co_entitlements_get(snowflake user_id, const std::vector& sku_ids, snowflake before_id, snowflake after_id, uint8_t limit, snowflake guild_id, bool exclude_ended) { + return async{ this, static_cast&, snowflake, snowflake, uint8_t, snowflake, bool, command_completion_event_t)>(&cluster::entitlements_get), user_id, sku_ids, before_id, after_id, limit, guild_id, exclude_ended }; +} + +async cluster::co_entitlement_test_create(const class entitlement& new_entitlement) { + return async{ this, static_cast(&cluster::entitlement_test_create), new_entitlement }; +} + +async cluster::co_entitlement_test_delete(const class snowflake entitlement_id) { + return async{ this, static_cast(&cluster::entitlement_test_delete), entitlement_id }; +} + async cluster::co_get_gateway_bot() { return async{ this, static_cast(&cluster::get_gateway_bot) }; } @@ -567,6 +579,10 @@ async cluster::co_guild_event_get(snowflake guild_id, s return async{ this, static_cast(&cluster::guild_event_get), guild_id, event_id }; } +async cluster::co_skus_get() { + return async{ this, static_cast(&cluster::skus_get) }; +} + async cluster::co_stage_instance_create(const stage_instance& si) { return async{ this, static_cast(&cluster::stage_instance_create), si }; } diff --git a/src/dpp/cluster_sync_calls.cpp b/src/dpp/cluster_sync_calls.cpp index 0167edddb2..66e057c1ee 100644 --- a/src/dpp/cluster_sync_calls.cpp +++ b/src/dpp/cluster_sync_calls.cpp @@ -257,6 +257,18 @@ emoji_map cluster::guild_emojis_get_sync(snowflake guild_id) { return dpp::sync(this, static_cast(&cluster::guild_emojis_get), guild_id); } +entitlement_map cluster::entitlements_get_sync(snowflake user_id, const std::vector& sku_ids, snowflake before_id, snowflake after_id, uint8_t limit, snowflake guild_id, bool exclude_ended) { + return dpp::sync(this, static_cast&, snowflake, snowflake, uint8_t, snowflake, bool, command_completion_event_t)>(&cluster::entitlements_get), user_id, sku_ids, before_id, after_id, limit, guild_id, exclude_ended); +} + +entitlement cluster::entitlement_test_create_sync(const class entitlement& new_entitlement) { + return dpp::sync(this, static_cast(&cluster::entitlement_test_create), new_entitlement); +} + +confirmation cluster::entitlement_test_delete_sync(const class snowflake entitlement_id) { + return dpp::sync(this, static_cast(&cluster::entitlement_test_delete), entitlement_id); +} + gateway cluster::get_gateway_bot_sync() { return dpp::sync(this, static_cast(&cluster::get_gateway_bot)); } @@ -565,6 +577,10 @@ scheduled_event cluster::guild_event_get_sync(snowflake guild_id, snowflake even return dpp::sync(this, static_cast(&cluster::guild_event_get), guild_id, event_id); } +sku_map cluster::skus_get_sync() { + return dpp::sync(this, static_cast(&cluster::skus_get)); +} + stage_instance cluster::stage_instance_create_sync(const stage_instance& si) { return dpp::sync(this, static_cast(&cluster::stage_instance_create), si); } diff --git a/src/dpp/discordevents.cpp b/src/dpp/discordevents.cpp index 7ca54a0d04..18396ed225 100644 --- a/src/dpp/discordevents.cpp +++ b/src/dpp/discordevents.cpp @@ -390,6 +390,9 @@ const std::map eventmap = { { "AUTO_MODERATION_RULE_DELETE", new dpp::events::automod_rule_delete() }, { "AUTO_MODERATION_ACTION_EXECUTION", new dpp::events::automod_rule_execute() }, { "GUILD_AUDIT_LOG_ENTRY_CREATE", new dpp::events::guild_audit_log_entry_create() }, + { "ENTITLEMENT_CREATE", new dpp::events::entitlement_create() }, + { "ENTITLEMENT_UPDATE", new dpp::events::entitlement_update() }, + { "ENTITLEMENT_DELETE", new dpp::events::entitlement_delete() }, }; void discord_client::handle_event(const std::string &event, json &j, const std::string &raw) diff --git a/src/dpp/entitlement.cpp b/src/dpp/entitlement.cpp new file mode 100644 index 0000000000..245eb3b383 --- /dev/null +++ b/src/dpp/entitlement.cpp @@ -0,0 +1,78 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include +#include +#include + +namespace dpp { + +using json = nlohmann::json; + +entitlement::entitlement(const snowflake _sku_id, const snowflake _id, const snowflake _application_id, const entitlement_type _type, const uint8_t _flags) + : managed(_id), sku_id(_sku_id), application_id(_application_id), type(_type), flags(_flags) {} + +entitlement& entitlement::fill_from_json_impl(nlohmann::json* j) { + set_snowflake_not_null(j, "id", id); + set_snowflake_not_null(j, "sku_id", sku_id); + set_snowflake_not_null(j, "application_id", application_id); + + /* Discord does separate these values, but asks for them as "owner_id" in the create event, just makes sense to make them as one as only one is ever set. */ + if(snowflake_not_null(j, "user_id")) { + set_snowflake_not_null(j, "user_id", owner_id); + } else if(snowflake_not_null(j, "guild_id")) { + set_snowflake_not_null(j, "guild_id", owner_id); + } + + type = static_cast(int8_not_null(j, "type")); + + if (bool_not_null(j, "deleted")) { + flags |= ent_deleted; + } + + set_ts_not_null(j, "starts_at", starts_at); + set_ts_not_null(j, "ends_at", ends_at); + + /* + * TODO: Look at the entitlement example on docs and see what we're missing, add it here after. Discord seems to be missing information in their structure as their example shows more data. + */ + + return *this; +} + +json entitlement::to_json_impl(bool with_id) const { + json j; + if (with_id) { + j["id"] = id.str(); + } + j["sku_id"] = sku_id.str(); + return j; +} + +entitlement_type entitlement::get_type() const { + return type; +} + +bool entitlement::is_deleted() const { + return flags & entitlement_flags::ent_deleted; +} + +} // namespace dpp diff --git a/src/dpp/events/entitlement_create.cpp b/src/dpp/events/entitlement_create.cpp new file mode 100644 index 0000000000..5356f88b98 --- /dev/null +++ b/src/dpp/events/entitlement_create.cpp @@ -0,0 +1,47 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include +#include +#include + +namespace dpp::events { + +/** + * @brief Handle event + * + * @param client Websocket client (current shard) + * @param j JSON data for the event + * @param raw Raw JSON string + */ +void entitlement_create::handle(discord_client* client, json &j, const std::string &raw) { + if (!client->creator->on_entitlement_create.empty()) { + dpp::entitlement ent; + ent.fill_from_json(&j); + + dpp::entitlement_create_t entitlement_event(client, raw); + entitlement_event.created = ent; + + client->creator->on_entitlement_create.call(entitlement_event); + } +} + +}; diff --git a/src/dpp/events/entitlement_delete.cpp b/src/dpp/events/entitlement_delete.cpp new file mode 100644 index 0000000000..d1c645faee --- /dev/null +++ b/src/dpp/events/entitlement_delete.cpp @@ -0,0 +1,47 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include +#include +#include + +namespace dpp::events { + +/** + * @brief Handle event + * + * @param client Websocket client (current shard) + * @param j JSON data for the event + * @param raw Raw JSON string + */ +void entitlement_delete::handle(discord_client* client, json &j, const std::string &raw) { + if (!client->creator->on_entitlement_delete.empty()) { + dpp::entitlement ent; + ent.fill_from_json(&j); + + dpp::entitlement_delete_t entitlement_event(client, raw); + entitlement_event.deleted = ent; + + client->creator->on_entitlement_delete.call(entitlement_event); + } +} + +}; diff --git a/src/dpp/events/entitlement_update.cpp b/src/dpp/events/entitlement_update.cpp new file mode 100644 index 0000000000..b4921eb4dc --- /dev/null +++ b/src/dpp/events/entitlement_update.cpp @@ -0,0 +1,47 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include +#include +#include + +namespace dpp::events { + +/** + * @brief Handle event + * + * @param client Websocket client (current shard) + * @param j JSON data for the event + * @param raw Raw JSON string + */ +void entitlement_update::handle(discord_client* client, json &j, const std::string &raw) { + if (!client->creator->on_entitlement_update.empty()) { + dpp::entitlement ent; + ent.fill_from_json(&j); + + dpp::entitlement_update_t entitlement_event(client, raw); + entitlement_event.updating_entitlement = ent; + + client->creator->on_entitlement_update.call(entitlement_event); + } +} + +}; diff --git a/src/dpp/sku.cpp b/src/dpp/sku.cpp new file mode 100644 index 0000000000..170a158159 --- /dev/null +++ b/src/dpp/sku.cpp @@ -0,0 +1,89 @@ +/************************************************************************************ + * + * D++, A Lightweight C++ library for Discord + * + * SPDX-License-Identifier: Apache-2.0 + * Copyright 2021 Craig Edwards and D++ contributors + * (https://github.com/brainboxdotcc/DPP/graphs/contributors) + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + ************************************************************************************/ +#include +#include +#include + +namespace dpp { + +using json = nlohmann::json; + +sku::sku(const snowflake _id, const sku_type _type, const snowflake _application_id, const std::string _name, const std::string _slug, const uint16_t _flags) + : managed(_id), type(_type), application_id(_application_id), name(_name), slug(_slug), flags(_flags) {} + +sku& sku::fill_from_json_impl(nlohmann::json* j) { + set_snowflake_not_null(j, "id", id); + + type = static_cast(int8_not_null(j, "type")); + + set_snowflake_not_null(j, "application_id", application_id); + + set_string_not_null(j, "name", name); + set_string_not_null(j, "slug", slug); + + uint8_t sku_flags = int8_not_null(j, "flags"); + if (sku_flags & (1 << 2)) { + flags |= sku_flags::sku_available; + } + if (sku_flags & (1 << 7)) { + flags |= sku_flags::sku_guild_subscription; + } + if (sku_flags & (1 << 8)) { + flags |= sku_flags::sku_user_subscription; + } + + /* + * TODO: Look at the SKU example on docs and see what we're missing, add it here after. Discord seems to be missing information in their structure as their example shows more data. + * yes this is copied from dpp/entitlement.cpp, Discord's fault for their docs being inconsistent. + */ + + return *this; +} + +json sku::to_json_impl(bool with_id) const { + json j; + if (with_id) { + j["id"] = id.str(); + } + + /* There's no reason to ever use this as you can only get SKUs, so no point putting any data here */ + + return j; +} + +sku_type sku::get_type() const { + return type; +} + +bool sku::is_available() const { + return flags & sku_flags::sku_available; +} + +bool sku::is_guild_subscription() const { + return flags & sku_flags::sku_guild_subscription; +} + +bool sku::is_user_subscription() const { + return flags & sku_flags::sku_user_subscription; +} + +} // namespace dpp diff --git a/src/dpp/slashcommand.cpp b/src/dpp/slashcommand.cpp index 77dde316fb..f3a8118145 100644 --- a/src/dpp/slashcommand.cpp +++ b/src/dpp/slashcommand.cpp @@ -593,12 +593,12 @@ void from_json(const nlohmann::json& j, interaction& i) { if (j.contains("channel") && !j.at("channel").is_null()) { const json& c = j["channel"]; - i.channel = channel().fill_from_json((json*)&c); + i.channel = channel().fill_from_json(const_cast(&c)); } if (j.contains("message") && !j.at("message").is_null()) { const json& m = j["message"]; - i.msg = message().fill_from_json((json*)&m, i.cache_policy); + i.msg = message().fill_from_json(const_cast(&m), i.cache_policy); set_snowflake_not_null(&m, "id", i.message_id); } @@ -719,6 +719,12 @@ void from_json(const nlohmann::json& j, interaction& i) { i.data = ai; } } + + if(j.contains("entitlements")) { + for (auto& entitle : j["entitlements"]) { + i.entitlements.emplace_back(entitlement().fill_from_json(const_cast(&entitle))); + } + } } interaction_response& interaction_response::add_autocomplete_choice(const command_option_choice& achoice) { From c90d17afb13597ef1f0752771de15c26fea4fabf Mon Sep 17 00:00:00 2001 From: Miuna <809711+Mishura4@users.noreply.github.com> Date: Wed, 25 Oct 2023 06:22:44 -0400 Subject: [PATCH 14/14] fix(#957): support iconhash/image data variant fields (#965) --- include/dpp/emoji.h | 14 +- include/dpp/guild.h | 118 +++++++++++++- include/dpp/role.h | 73 ++++++--- include/dpp/scheduled_event.h | 27 +++- include/dpp/user.h | 2 +- include/dpp/utility.h | 280 +++++++++++++++++++++++++++++++--- src/dpp/emoji.cpp | 12 +- src/dpp/guild.cpp | 135 ++++++++++++---- src/dpp/role.cpp | 59 +++---- src/dpp/scheduled_event.cpp | 17 ++- src/dpp/utility.cpp | 128 +++++++++++++++- src/unittest/test.cpp | 38 ++++- src/unittest/test.h | 7 +- src/unittest/unittest.cpp | 8 +- 14 files changed, 770 insertions(+), 148 deletions(-) diff --git a/include/dpp/emoji.h b/include/dpp/emoji.h index b8eba59cf5..fff307d0f4 100644 --- a/include/dpp/emoji.h +++ b/include/dpp/emoji.h @@ -89,7 +89,7 @@ class DPP_EXPORT emoji : public managed, public json_interface { /** * @brief Image data for the emoji, if uploading. */ - std::string image_data; + utility::image_data image_data; /** * @brief Flags for the emoji from dpp::emoji_flags. @@ -185,7 +185,7 @@ class DPP_EXPORT emoji : public managed, public json_interface { bool is_available() const; /** - * @brief Load an image into the object as base64 + * @brief Load an image into the object * * @param image_blob Image binary data * @param type Type of image. It can be one of `i_gif`, `i_jpg` or `i_png`. @@ -194,6 +194,16 @@ class DPP_EXPORT emoji : public managed, public json_interface { */ emoji& load_image(std::string_view image_blob, const image_type type); + /** + * @brief Load an image into the object + * + * @param image_blob Image binary data + * @param type Type of image. It can be one of `i_gif`, `i_jpg` or `i_png`. + * @return emoji& Reference to self + * @throw dpp::length_exception Image content exceeds discord maximum of 256 kilobytes + */ + emoji& load_image(const std::byte* data, uint32_t size, const image_type type); + /** * @brief Format to name if unicode, name:id if has id or a:name:id if animated * diff --git a/include/dpp/guild.h b/include/dpp/guild.h index b68d1181dc..fd6a3d8a98 100644 --- a/include/dpp/guild.h +++ b/include/dpp/guild.h @@ -713,17 +713,17 @@ class DPP_EXPORT guild : public managed, public json_interface { */ dpp::welcome_screen welcome_screen; - /** Guild icon hash */ - utility::iconhash icon; + /** Guild icon */ + utility::icon icon; - /** Guild splash hash */ - utility::iconhash splash; + /** Guild splash */ + utility::icon splash; - /** Guild discovery splash hash */ - utility::iconhash discovery_splash; + /** Guild discovery splash */ + utility::icon discovery_splash; - /** Server banner hash */ - utility::iconhash banner; + /** Server banner */ + utility::icon banner; /** Snowflake id of guild owner */ snowflake owner_id; @@ -945,6 +945,108 @@ class DPP_EXPORT guild : public managed, public json_interface { */ guild& set_name(const std::string& n); + /** + * @brief Remove the guild banner. + * @return guild& Reference to self for chaining + */ + guild& remove_banner(); + + /** + * @brief Set the guild banner image. Server needs banner feature. + * Must be 16:9, and depending on nitro level, must be png or jpeg. + * Animated gif needs the animated banner server feature. + * @param format Image format. + * @param data Image data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_banner(image_type format, std::string_view data); + + /** + * @brief Set the guild banner image. Server needs banner feature. + * Must be 16:9, and depending on nitro level, must be png or jpeg. + * Animated gif needs the animated banner server feature. + * @param format Image format. + * @param data Image data in bytes + * @param size Size of the data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_banner(image_type format, const std::byte* data, uint32_t size); + + /** + * @brief Remove the guild discovery splash. + * @return guild& Reference to self for chaining + */ + guild& remove_discovery_splash(); + + /** + * @brief Set the guild discovery splash image. Server needs discoverable feature. + * Must be 16:9 and png or jpeg. + * @param format Image format. + * @param data Image data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_discovery_splash(image_type format, std::string_view data); + + /** + * @brief Set the guild discovery splash image. Server needs discoverable feature. + * Must be 16:9 and png or jpeg. + * @param format Image format. + * @param data Image data in bytes + * @param size Size of the data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_discovery_splash(image_type format, const std::byte* data, uint32_t size); + + /** + * @brief Remove the guild invite splash. + * @return guild& Reference to self for chaining + */ + guild& remove_splash(); + + /** + * @brief Set the guild invite splash image. Server needs invite splash feature. + * Must be 16:9 and png or jpeg. + * @param format Image format. + * @param data Image data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_splash(image_type format, std::string_view data); + + /** + * @brief Set the guild invite splash image. Server needs invite splash feature. + * Must be 16:9 and png or jpeg. + * @param format Image format. + * @param data Image data in bytes + * @param size Size of the data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_splash(image_type format, const std::byte* data, uint32_t size); + + /** + * @brief Remove the guild icon. + * @return guild& Reference to self for chaining + */ + guild& remove_icon(); + + /** + * @brief Set the guild icon image. + * Must be 1024x1024 and png or jpeg. Gif allowed only if the server has animated icon. + * @param format Image format. + * @param data Image data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_icon(image_type format, std::string_view data); + + /** + * @brief Set the 1024x1024 guild icon image. + * Must be png or jpeg. Gif allowed only if the server has animated icon. + * @param format Image format. + * @param data Image data in bytes + * @param size Size of the data in bytes + * @return guild& Reference to self for chaining + */ + guild& set_icon(image_type format, const std::byte* data, uint32_t size); + /** * @brief Is a large server (>250 users) * @return bool is a large guild diff --git a/include/dpp/role.h b/include/dpp/role.h index 6a9b7d093e..069ab2f446 100644 --- a/include/dpp/role.h +++ b/include/dpp/role.h @@ -74,45 +74,71 @@ class DPP_EXPORT role : public managed, public json_interface { * @brief Role name * Between 1 and 100 characters. */ - std::string name; + std::string name{}; /** * @brief Guild ID */ - snowflake guild_id; + snowflake guild_id{0}; /** * @brief Role colour. * A colour of 0 means no colour. If you want a black role, * you must use the value 0x000001. */ - uint32_t colour; + uint32_t colour{0}; /** Role position */ - uint8_t position; + uint8_t position{0}; /** Role permissions bitmask values from dpp::permissions */ - permission permissions; + permission permissions{}; /** Role flags from dpp::role_flags */ - uint8_t flags; + uint8_t flags{0}; /** Integration id if any (e.g. role is a bot's role created when it was invited) */ - snowflake integration_id; + snowflake integration_id{}; /** Bot id if any (e.g. role is a bot's role created when it was invited) */ - snowflake bot_id; + snowflake bot_id{}; /** The id of the role's subscription sku and listing */ - snowflake subscription_listing_id; + snowflake subscription_listing_id{}; /** The unicode emoji used for the role's icon, can be an empty string */ - std::string unicode_emoji; - /** The role icon hash, can be an empty string */ - utility::iconhash icon; - /** Image data for the role icon (if any) */ - std::string* image_data; + std::string unicode_emoji{}; + /** The role icon */ + utility::icon icon{}; /** * @brief Construct a new role object */ - role(); + role() = default; + + /** + * @brief Construct a new role object. + * + * @param rhs Role object to copy + */ + role(const role& rhs) = default; + + /** + * @brief Construct a new role object. + * + * @param rhs Role object to move + */ + role(role&& rhs) = default; + + /** + * @brief Copy another role object + * + * @param rhs Role object to copy + */ + role &operator=(const role& rhs) = default; + + /** + * @brief Move from another role object + * + * @param rhs Role object to copy + */ + role &operator=(role&& rhs) = default; /** * @brief Destroy the role object */ - virtual ~role(); + virtual ~role() = default; /** * @brief Create a mentionable role. @@ -210,13 +236,22 @@ class DPP_EXPORT role : public managed, public json_interface { std::string get_icon_url(uint16_t size = 0, const image_type format = i_png) const; /** - * @brief Load an image into the object as base64 - * + * @brief Load a role icon + * + * @param image_blob Image binary data + * @param type Type of image. It can be one of `i_gif`, `i_jpg` or `i_png`. + * @return emoji& Reference to self + */ + role& load_image(std::string_view image_blob, const image_type type); + + /** + * @brief Load a role icon + * * @param image_blob Image binary data * @param type Type of image. It can be one of `i_gif`, `i_jpg` or `i_png`. * @return emoji& Reference to self */ - role& load_image(const std::string &image_blob, const image_type type); + role& load_image(const std::byte* data, uint32_t size, const image_type type); /** * @brief Operator less than, used for checking if a role is below another. diff --git a/include/dpp/scheduled_event.h b/include/dpp/scheduled_event.h index 56dc7b039a..c8e8565cbd 100644 --- a/include/dpp/scheduled_event.h +++ b/include/dpp/scheduled_event.h @@ -112,7 +112,7 @@ struct DPP_EXPORT scheduled_event : public managed, public json_interface { uint32_t flags; /** Discriminator (aka tag), 4 digits usually displayed with leading zeroes. * - * @note To print the discriminator with leading zeroes, use format_username(). + * @note To print the discriminator with leading zeroes, use format_username(). * 0 for users that have migrated to the new username format. */ uint16_t discriminator; diff --git a/include/dpp/utility.h b/include/dpp/utility.h index 1410b8599e..aec94a32e3 100644 --- a/include/dpp/utility.h +++ b/include/dpp/utility.h @@ -29,6 +29,9 @@ #include #include #include +#include +#include +#include /** * @brief The main namespace for D++ functions, classes and types @@ -215,33 +218,24 @@ std::string DPP_EXPORT loglevel(dpp::loglevel in); * the value back in string form. */ struct DPP_EXPORT iconhash { - - uint64_t first; //!< High 64 bits - uint64_t second; //!< Low 64 bits + /** @brief High 64 bits */ + uint64_t first; + /** @brief Low 64 bits */ + uint64_t second; /** * @brief Construct a new iconhash object * @param _first Leftmost portion of the hash value * @param _second Rightmost portion of the hash value */ - iconhash(uint64_t _first = 0, uint64_t _second = 0); - - /** - * @brief Construct a new iconhash object - */ - iconhash(const iconhash&); - - /** - * @brief Destroy the iconhash object - */ - ~iconhash(); + iconhash(uint64_t _first = 0, uint64_t _second = 0) noexcept; /** * @brief Construct a new iconhash object - * + * * @param hash String hash to construct from. * Must contain a 32 character hex string. - * + * * @throws std::length_error if the provided * string is not exactly 32 characters long. */ @@ -249,9 +243,9 @@ struct DPP_EXPORT iconhash { /** * @brief Assign from std::string - * + * * @param assignment string to assign from. - * + * * @throws std::length_error if the provided * string is not exactly 32 characters long. */ @@ -259,18 +253,18 @@ struct DPP_EXPORT iconhash { /** * @brief Check if one iconhash is equal to another - * + * * @param other other iconhash to compare * @return True if the iconhash objects match */ - bool operator==(const iconhash& other) const; + bool operator==(const iconhash& other) const noexcept; /** * @brief Change value of iconhash object - * + * * @param hash String hash to change to. * Must contain a 32 character hex string. - * + * * @throws std::length_error if the provided * string is not exactly 32 characters long. */ @@ -279,12 +273,250 @@ struct DPP_EXPORT iconhash { /** * @brief Convert iconhash back to 32 character * string value. - * - * @return std::string Hash value + * + * @return std::string Hash value */ std::string to_string() const; }; +/** + * @brief Image to be received or sent to API calls. + * + * This class is carefully crafted to be 16 bytes, + * this is why we use a ptr + 4 byte size instead of a vector. + * We want this class to be substitutable with iconhash in data structures. + */ +struct DPP_EXPORT image_data { + /** + * @brief Data in bytes of the image. + */ + std::unique_ptr data = nullptr; + + /** + * @brief Size of the data in bytes. + */ + uint32_t size = 0; + + /** + * @brief Type of the image. + * + * @see image_type + */ + image_type type = {}; + + /** + * @brief Construct an empty image. + */ + image_data() = default; + + /** + * @brief Copy an image. + * + * @param rhs Image to copy + */ + image_data(const image_data& rhs); + + /** + * @brief Move an image. + * + * @param rhs Image to copy + */ + image_data(image_data&&) noexcept = default; + + /** + * @brief Construct from string buffer + * + * @param format Image format + * @param str Data in a string + * @see image_type + */ + image_data(image_type format, std::string_view bytes); + + /** + * @brief Construct from byte buffer + * + * @param format Image format + * @param buf Byte buffer + * @param size_t Image size in bytes + * @see image_type + */ + image_data(image_type format, const std::byte* bytes, uint32_t byte_size); + + /** + * @brief Copy an image data. + * + * @param rhs Image to copy + * @return self for chaining + */ + image_data& operator=(const image_data& rhs); + + /** + * @brief Move an image data. + * + * @param rhs Image to move from + * @return self for chaining + */ + image_data& operator=(image_data&& rhs) noexcept = default; + + /** + * @brief Set image data. + * + * @param format Format of the image + * @param data Data of the image + */ + void set(image_type format, std::string_view bytes); + + /** + * @brief Set image data. + * + * @param format Format of the image + * @param data Data of the image + */ + void set(image_type format, const std::byte* bytes, uint32_t byte_size); + + /** + * @brief Encode to base64. + * + * @return std::string New string with the image data encoded in base64 + */ + std::string base64_encode() const; + + /** + * @brief Get the file extension. + * + * Alias for \ref file_extension + * @return std::string File extension e.g. `.png` + */ + std::string get_file_extension() const; + + /** + * @brief Get the mime type. + * + * Alias for \ref mime_type + * @return std::string File mime type e.g. "image/png" + */ + std::string get_mime_type() const; + + /** + * @brief Check if this is an empty image. + * + * @return bool Whether the image is empty or not + */ + bool empty() const noexcept; + + /** + * @brief Build a data URI scheme suitable for sending to Discord + * + * @see https://discord.com/developers/docs/reference#image-data + * @return The data URI scheme as a json or null if empty + */ + json to_nullable_json() const; +}; + +/** + * @brief Wrapper class around a variant for either iconhash or image, + * for API objects that have one or the other (generally iconhash when receiving, + * image when uploading an image) + */ +struct icon { + /** + * @brief Iconhash received or image data for upload. + */ + std::variant hash_or_data; + + /** + * @brief Assign to iconhash. + * + * @param hash Iconhash + */ + icon& operator=(const iconhash& hash); + + /** + * @brief Assign to iconhash. + * + * @param hash Iconhash + */ + icon& operator=(iconhash&& hash) noexcept; + + /** + * @brief Assign to image. + * + * @param img Image + */ + icon& operator=(const image_data& img); + + /** + * @brief Assign to image. + * + * @param img Image + */ + icon& operator=(image_data&& img) noexcept; + + /** + * @brief Check whether this icon is stored as an iconhash + * + * @see iconhash + * @return bool Whether this icon is stored as an iconhash + */ + bool is_iconhash() const; + + /** + * @brief Get as icon hash. + * + * @warn The behavior is undefined if `is_iconhash() == false` + * @return iconhash& This iconhash + */ + iconhash& as_iconhash() &; + + /** + * @brief Get as icon hash. + * + * @warn The behavior is undefined if `is_iconhash() == false` + * @return iconhash& This iconhash + */ + const iconhash& as_iconhash() const&; + + /** + * @brief Get as icon hash. + * + * @warn The behavior is undefined if `is_iconhash() == false` + * @return iconhash& This iconhash + */ + iconhash&& as_iconhash() &&; + + /** + * @brief Check whether this icon is stored as an image + * + * @see image_data + * @return bool Whether this icon is stored as an image + */ + bool is_image_data() const; + + /** + * @brief Get as image data. + * + * @warn The behavior is undefined if `is_image_data() == false` + * @return image_data& This image + */ + image_data& as_image_data() &; + + /** + * @brief Get as image. + * + * @warn The behavior is undefined if `is_image_data() == false` + * @return image_data& This image + */ + const image_data& as_image_data() const&; + + /** + * @brief Get as image. + * + * @warn The behavior is undefined if `is_image_data() == false` + * @return image_data& This image + */ + image_data&& as_image_data() &&; +}; + /** * @brief Return the current time with fractions of seconds. * This is a unix epoch time with the fractional seconds part diff --git a/src/dpp/emoji.cpp b/src/dpp/emoji.cpp index 7793c34c85..3d6280e08e 100644 --- a/src/dpp/emoji.cpp +++ b/src/dpp/emoji.cpp @@ -70,7 +70,7 @@ json emoji::to_json_impl(bool with_id) const { } j["name"] = name; if (!image_data.empty()) { - j["image"] = image_data; + j["image"] = image_data.to_nullable_json(); } j["roles"] = json::array(); for (const auto& role : roles) { @@ -99,9 +99,15 @@ emoji& emoji::load_image(std::string_view image_blob, const image_type type) { if (image_blob.size() > MAX_EMOJI_SIZE) { throw dpp::length_exception("Emoji file exceeds discord limit of 256 kilobytes"); } + image_data = utility::image_data{type, image_blob}; + return *this; +} - image_data = "data:" + utility::mime_type(type) + ";base64," + base64_encode(reinterpret_cast(image_blob.data()), static_cast(image_blob.length())); - +emoji& emoji::load_image(const std::byte *data, uint32_t size, const image_type type) { + if (size > MAX_EMOJI_SIZE) { + throw dpp::length_exception("Emoji file exceeds discord limit of 256 kilobytes"); + } + image_data = utility::image_data{type, data, size}; return *this; } diff --git a/src/dpp/guild.cpp b/src/dpp/guild.cpp index d919618fe3..a670c4f5a0 100644 --- a/src/dpp/guild.cpp +++ b/src/dpp/guild.cpp @@ -164,7 +164,7 @@ guild_member& guild_member::set_communication_disabled_until(const time_t disabl this->communication_disabled_until = disabled_timestamp; return *this; } - + bool guild_member::operator == (guild_member const& other_member) const { if ((this->user_id == other_member.user_id && this->user_id.empty()) || (this->guild_id == other_member.guild_id && this->guild_id.empty())) return false; @@ -267,6 +267,66 @@ guild& guild::set_name(const std::string& n) { return *this; } +guild &guild::remove_banner() { + this->banner = utility::image_data{}; + return *this; +} + +guild& guild::set_banner(image_type format, std::string_view data) { + this->banner = utility::image_data{format, data}; + return *this; +} + +guild& guild::set_banner(image_type format, const std::byte* data, uint32_t size) { + this->banner = utility::image_data{format, data, size}; + return *this; +} + +guild &guild::remove_discovery_splash() { + this->discovery_splash = utility::image_data{}; + return *this; +} + +guild& guild::set_discovery_splash(image_type format, std::string_view data) { + this->discovery_splash = utility::image_data{format, data}; + return *this; +} + +guild& guild::set_discovery_splash(image_type format, const std::byte* data, uint32_t size) { + this->discovery_splash = utility::image_data{format, data, size}; + return *this; +} + +guild &guild::remove_splash() { + this->splash = utility::image_data{}; + return *this; +} + +guild& guild::set_splash(image_type format, std::string_view data) { + this->splash = utility::image_data{format, data}; + return *this; +} + +guild& guild::set_splash(image_type format, const std::byte* data, uint32_t size) { + this->splash = utility::image_data{format, data, size}; + return *this; +} + +guild &guild::remove_icon() { + this->icon = utility::image_data{}; + return *this; +} + +guild& guild::set_icon(image_type format, std::string_view data) { + this->icon = utility::image_data{format, data}; + return *this; +} + +guild& guild::set_icon(image_type format, const std::byte* data, uint32_t size) { + this->icon = utility::image_data{format, data, size}; + return *this; +} + user* guild_member::get_user() const { return find_user(user_id); } @@ -549,6 +609,18 @@ json guild::to_json_impl(bool with_id) const { if (!safety_alerts_channel_id.empty()) { j["safety_alerts_channel_id"] = safety_alerts_channel_id; } + if (banner.is_image_data()) { + j["banner"] = banner.as_image_data().to_nullable_json(); + } + if (discovery_splash.is_image_data()) { + j["discovery_splash"] = discovery_splash.as_image_data().to_nullable_json(); + } + if (splash.is_image_data()) { + j["splash"] = splash.as_image_data().to_nullable_json(); + } + if (icon.is_image_data()) { + j["icon"] = icon.as_image_data().to_nullable_json(); + } return j; } @@ -890,43 +962,55 @@ bool guild::connect_member_voice(snowflake user_id, bool self_mute, bool self_de } std::string guild::get_banner_url(uint16_t size, const image_type format, bool prefer_animated) const { - if (!this->banner.to_string().empty() && this->id) { - return utility::cdn_endpoint_url_hash({ i_jpg, i_png, i_webp, i_gif }, - "banners/" + std::to_string(this->id), this->banner.to_string(), - format, size, prefer_animated, has_animated_banner_hash()); - } else { - return std::string(); + if (this->banner.is_iconhash() && this->id) { + std::string as_str = this->banner.as_iconhash().to_string(); + + if (!as_str.empty()) { + return utility::cdn_endpoint_url_hash({ i_jpg, i_png, i_webp, i_gif }, + "banners/" + std::to_string(this->id), as_str, + format, size, prefer_animated, has_animated_banner_hash()); + } } + return std::string{}; } std::string guild::get_discovery_splash_url(uint16_t size, const image_type format) const { - if (!this->discovery_splash.to_string().empty() && this->id) { - return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp }, - "discovery-splashes/" + std::to_string(this->id) + "/" + this->discovery_splash.to_string(), - format, size); - } else { - return std::string(); + if (this->discovery_splash.is_iconhash() && this->id) { + std::string as_str = this->discovery_splash.as_iconhash().to_string(); + + if (!as_str.empty()) { + return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp }, + "discovery-splashes/" + std::to_string(this->id) + "/" + as_str, + format, size); + } } + return std::string{}; } std::string guild::get_icon_url(uint16_t size, const image_type format, bool prefer_animated) const { - if (!this->icon.to_string().empty() && this->id) { - return utility::cdn_endpoint_url_hash({ i_jpg, i_png, i_webp, i_gif }, - "icons/" + std::to_string(this->id), this->icon.to_string(), - format, size, prefer_animated, has_animated_icon_hash()); - } else { - return std::string(); + if (this->icon.is_iconhash() && this->id) { + std::string as_str = this->icon.as_iconhash().to_string(); + + if (!as_str.empty()) { + return utility::cdn_endpoint_url_hash({ i_jpg, i_png, i_webp, i_gif }, + "icons/" + std::to_string(this->id), as_str, + format, size, prefer_animated, has_animated_icon_hash()); + } } + return std::string{}; } std::string guild::get_splash_url(uint16_t size, const image_type format) const { - if (!this->splash.to_string().empty() && this->id) { - return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp, i_gif }, - "splashes/" + std::to_string(this->id) + "/" + this->splash.to_string(), - format, size); - } else { - return std::string(); + if (this->splash.is_iconhash() && this->id) { + std::string as_str = this->splash.as_iconhash().to_string(); + + if (!as_str.empty()) { + return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp, i_gif }, + "splashes/" + std::to_string(this->id) + "/" + as_str, + format, size); + } } + return std::string{}; } guild_member find_guild_member(const snowflake guild_id, const snowflake user_id) { @@ -939,7 +1023,6 @@ guild_member find_guild_member(const snowflake guild_id, const snowflake user_id throw dpp::cache_exception("Requested member not found in the guild cache!"); } - throw dpp::cache_exception("Requested guild cache not found!"); } diff --git a/src/dpp/role.cpp b/src/dpp/role.cpp index 501288675b..99e87ffec1 100644 --- a/src/dpp/role.cpp +++ b/src/dpp/role.cpp @@ -38,25 +38,6 @@ std::map rolemap = { { 1 << 0, dpp::r_in_prompt }, }; -role::role() : - managed(), - guild_id(0), - colour(0), - position(0), - permissions(0), - flags(0), - integration_id(0), - bot_id(0), - subscription_listing_id(0), - image_data(nullptr) -{ -} - -role::~role() -{ - delete image_data; -} - std::string role::get_mention(const snowflake& id){ return utility::role_mention(id); } @@ -70,7 +51,8 @@ role& role::fill_from_json(snowflake _guild_id, nlohmann::json* j) { this->guild_id = _guild_id; this->name = string_not_null(j, "name"); - this->icon = string_not_null(j, "icon"); + if (auto it = j->find("icon"); it != j->end() && !it->is_null()) + this->icon = utility::iconhash{it->get()}; this->unicode_emoji = string_not_null(j, "unicode_emoji"); this->id = snowflake_not_null(j, "id"); this->colour = int32_not_null(j, "color"); @@ -126,8 +108,8 @@ json role::to_json_impl(bool with_id) const { j["permissions"] = permissions; j["hoist"] = is_hoisted(); j["mentionable"] = is_mentionable(); - if (image_data) { - j["icon"] = *image_data; + if (icon.is_image_data()) { + j["icon"] = icon.as_image_data().to_nullable_json(); } if (!unicode_emoji.empty()) { j["unicode_emoji"] = unicode_emoji; @@ -140,19 +122,13 @@ std::string role::get_mention() const { return utility::role_mention(id); } -role& role::load_image(const std::string &image_blob, const image_type type) { - static const std::map mimetypes = { - { i_gif, "image/gif" }, - { i_jpg, "image/jpeg" }, - { i_png, "image/png" }, - { i_webp, "image/webp" }, - }; - - /* If there's already image data defined, free the old data, to prevent a memory leak */ - delete image_data; - - image_data = new std::string("data:" + mimetypes.find(type)->second + ";base64," + base64_encode((unsigned char const*)image_blob.data(), (unsigned int)image_blob.length())); +role& role::load_image(std::string_view image_blob, const image_type type) { + icon = utility::image_data{type, image_blob}; + return *this; +} +role& role::load_image(const std::byte* data, uint32_t size, const image_type type) { + icon = utility::image_data{type, data, size}; return *this; } @@ -424,13 +400,16 @@ members_container role::get_members() const { } std::string role::get_icon_url(uint16_t size, const image_type format) const { - if (!this->icon.to_string().empty() && this->id) { - return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp }, - "role-icons/" + std::to_string(this->id) + "/" + this->icon.to_string(), - format, size); - } else { - return std::string(); + if (this->icon.is_iconhash() && this->id) { + std::string as_str = this->icon.as_iconhash().to_string(); + + if (!as_str.empty()) { + return utility::cdn_endpoint_url({ i_jpg, i_png, i_webp }, + "role-icons/" + std::to_string(this->id) + "/" + as_str, + format, size); + } } + return std::string{}; } application_role_connection_metadata::application_role_connection_metadata() : key(""), name(""), description("") { diff --git a/src/dpp/scheduled_event.cpp b/src/dpp/scheduled_event.cpp index 6793d3547c..2bd8e03309 100644 --- a/src/dpp/scheduled_event.cpp +++ b/src/dpp/scheduled_event.cpp @@ -109,6 +109,16 @@ scheduled_event& scheduled_event::set_end_time(time_t t) { return *this; } +scheduled_event& scheduled_event::load_image(std::string_view image_blob, const image_type type) { + image = utility::image_data{type, image_blob}; + return *this; +} + +scheduled_event& scheduled_event::load_image(const std::byte* data, uint32_t size, const image_type type) { + image = utility::image_data{type, data, size}; + return *this; +} + scheduled_event& scheduled_event::fill_from_json_impl(const json* j) { set_snowflake_not_null(j, "id", this->id); set_snowflake_not_null(j, "guild_id", this->guild_id); @@ -117,7 +127,8 @@ scheduled_event& scheduled_event::fill_from_json_impl(const json* j) { set_snowflake_not_null(j, "creator_id", this->creator_id); set_string_not_null(j, "name", this->name); set_string_not_null(j, "description", this->description); - set_string_not_null(j, "image", this->image); + if (auto it = j->find("image"); it != j->end() && !it->is_null()) + this->image = utility::iconhash{it->get()}; set_ts_not_null(j, "scheduled_start_time", this->scheduled_start_time); set_ts_not_null(j, "scheduled_end_time", this->scheduled_end_time); this->privacy_level = static_cast(int8_not_null(j, "privacy_level")); @@ -144,8 +155,8 @@ json scheduled_event::to_json_impl(bool with_id) const { if (!this->description.empty()) { j["description"] = this->description; } - if (!this->image.empty()) { - j["image"] = this->image; + if (image.is_image_data()) { + j["image"] = image.as_image_data().to_nullable_json(); } j["privacy_level"] = this->privacy_level; j["status"] = this->status; diff --git a/src/dpp/utility.cpp b/src/dpp/utility.cpp index cb1ed83899..947f665962 100644 --- a/src/dpp/utility.cpp +++ b/src/dpp/utility.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -192,13 +193,9 @@ uint64_t uptime::to_msecs() const { return to_secs() * 1000; } -iconhash::iconhash(uint64_t _first, uint64_t _second) : first(_first), second(_second) { +iconhash::iconhash(uint64_t _first, uint64_t _second) noexcept : first(_first), second(_second) { } -iconhash::iconhash(const iconhash&) = default; - -iconhash::~iconhash() = default; - void iconhash::set(const std::string &hash) { std::string clean_hash(hash); if (hash.empty()) { // Clear values if empty hash @@ -227,7 +224,7 @@ iconhash& iconhash::operator=(const std::string &assignment) { return *this; } -bool iconhash::operator==(const iconhash& other) const { +bool iconhash::operator==(const iconhash& other) const noexcept { return other.first == first && other.second == second; } @@ -239,6 +236,125 @@ std::string iconhash::to_string() const { } } +namespace { + std::unique_ptr copy_data(const std::byte* data, size_t size) { + if (!data) + return nullptr; + std::unique_ptr ret = std::make_unique(size); + + std::copy_n(data, size, ret.get()); + return ret; + } + + template + std::unique_ptr copy_data(Range&& range) { + return copy_data(reinterpret_cast(std::data(range)), std::size(range)); + } +} + +image_data::image_data(const image_data& rhs) : data{copy_data(rhs.data.get(), rhs.size)}, size{rhs.size}, type{rhs.type} { +} + +image_data::image_data(image_type format, std::string_view str) : data{copy_data(str)}, size{static_cast(str.size())}, type{format} { +} + +image_data::image_data(image_type format, const std::byte* data, uint32_t byte_size) : data{copy_data(data, byte_size)}, size{byte_size}, type{format} { +} + +image_data& image_data::operator=(const image_data& rhs) { + data = copy_data(rhs.data.get(), rhs.size); + size = rhs.size; + type = rhs.type; + return *this; +} + +void image_data::set(image_type format, std::string_view bytes) { + data = copy_data(bytes); + size = static_cast(bytes.size()); +} + +void image_data::set(image_type format, const std::byte* bytes, uint32_t byte_size) { + data = copy_data(bytes, size); + size = static_cast(byte_size); +} + +std::string image_data::base64_encode() const { + return dpp::base64_encode(reinterpret_cast(data.get()), size); +} + +std::string image_data::get_file_extension() const { + return utility::file_extension(type); +} + +std::string image_data::get_mime_type() const { + return utility::mime_type(type); +} + +bool image_data::empty() const noexcept { + return (size == 0); +} + +json image_data::to_nullable_json() const { + if (empty()) { + return nullptr; + } + else { + return "data:" + get_mime_type() + ";base64," + base64_encode(); + } +} + +bool icon::is_iconhash() const { + return std::holds_alternative(hash_or_data); +} + +iconhash& icon::as_iconhash() & { + return std::get(hash_or_data); +} + +const iconhash& icon::as_iconhash() const& { + return std::get(hash_or_data); +} + +iconhash&& icon::as_iconhash() && { + return std::move(std::get(hash_or_data)); +} + +icon& icon::operator=(const iconhash& hash) { + hash_or_data = hash; + return *this; +} + +icon& icon::operator=(iconhash&& hash) noexcept { + hash_or_data = std::move(hash); + return *this; +} + +icon& icon::operator=(const image_data& img) { + hash_or_data = img; + return *this; +} + +icon& icon::operator=(image_data&& img) noexcept { + hash_or_data = std::move(img); + return *this; +} + +bool icon::is_image_data() const { + return std::holds_alternative(hash_or_data); +} + +image_data& icon::as_image_data() & { + return std::get(hash_or_data); +} + +const image_data& icon::as_image_data() const& { + return std::get(hash_or_data); +} + +image_data&& icon::as_image_data() && { + return std::move(std::get(hash_or_data)); +} + std::string debug_dump(uint8_t* data, size_t length) { std::ostringstream out; size_t addr = (size_t)data; diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 6775452153..fb599462a1 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -912,7 +912,7 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b coro_offline_tests(); } - std::vector test_image = load_test_image(); + std::vector dpp_logo = load_data("DPP-Logo.png"); set_test(PRESENCE, false); set_test(CLUSTER, false); @@ -959,6 +959,40 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b bot.on_voice_receive_combined([&](const auto& event) { }); + bot.on_guild_create([&](const dpp::guild_create_t& event) { + dpp::guild *g = event.created; + + if (g->id == TEST_GUILD_ID) { + start_test(GUILD_EDIT); + g->set_icon(dpp::i_png, dpp_logo.data(), static_cast(dpp_logo.size())); + bot.guild_edit(*g, [&bot](const dpp::confirmation_callback_t &result) { + if (result.is_error()) { + set_status(GUILD_EDIT, ts_failed, "guild_edit 1 errored:\n" + result.get_error().human_readable); + return; + } + dpp::guild g = result.get(); + + if (g.get_icon_url().empty()) { + set_status(GUILD_EDIT, ts_failed, "icon not set or not retrieved"); + return; + } + g.remove_icon(); + bot.guild_edit(g, [&bot](const dpp::confirmation_callback_t &result) { + if (result.is_error()) { + set_status(GUILD_EDIT, ts_failed, "guild_edit 2 errored:\n" + result.get_error().human_readable); + return; + } + const dpp::guild &g = result.get(); + if (!g.get_icon_url().empty()) { + set_status(GUILD_EDIT, ts_failed, "icon not removed"); + return; + } + set_status(GUILD_EDIT, ts_success); + }); + }); + } + }); + std::promise ready_promise; std::future ready_future = ready_promise.get_future(); bot.on_ready([&](const dpp::ready_t & event) { @@ -989,7 +1023,7 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b set_test(MESSAGERECEIVE, false); test_message.add_file("no-mime", "test"); test_message.add_file("test.txt", "test", "text/plain"); - test_message.add_file("test.png", std::string{test_image.begin(), test_image.end()}, "image/png"); + test_message.add_file("test.png", std::string{reinterpret_cast(dpp_logo.data()), dpp_logo.size()}, "image/png"); bot.message_create(test_message, [&bot](const dpp::confirmation_callback_t &callback) { if (!callback.is_error()) { set_test(MESSAGECREATE, true); diff --git a/src/unittest/test.h b/src/unittest/test.h index 7b2ee53e8b..9058632fd8 100644 --- a/src/unittest/test.h +++ b/src/unittest/test.h @@ -148,6 +148,7 @@ DPP_TEST(FORUM_CHANNEL_GET, "retrieve the created forum channel", tf_online); DPP_TEST(FORUM_CHANNEL_DELETE, "delete the created forum channel", tf_online); DPP_TEST(ERRORS, "Human readable error translation", tf_offline); +DPP_TEST(GUILD_EDIT, "cluster::guild_edit", tf_online); DPP_TEST(GUILD_BAN_CREATE, "cluster::guild_ban_add ban three deleted discord accounts", tf_online); DPP_TEST(GUILD_BAN_GET, "cluster::guild_get_ban getting one of the banned accounts", tf_online); DPP_TEST(GUILD_BANS_GET, "cluster::guild_get_bans get bans using the after-parameter", tf_online); @@ -471,11 +472,11 @@ int test_summary(); std::vector load_test_audio(); /** - * @brief Load test image for the attachment tests + * @brief Load bytes from file * - * @return std::vector data and size for test image + * @return std::vector File data */ -std::vector load_test_image(); +std::vector load_data(const std::string& file); /** * @brief Get the token from the environment variable DPP_UNIT_TEST_TOKEN diff --git a/src/unittest/unittest.cpp b/src/unittest/unittest.cpp index bd7c682f49..65059ad3fc 100644 --- a/src/unittest/unittest.cpp +++ b/src/unittest/unittest.cpp @@ -147,10 +147,10 @@ std::vector load_test_audio() { return testaudio; } -std::vector load_test_image() { - std::vector testimage; +std::vector load_data(const std::string& file) { + std::vector testimage; std::string dir = get_testdata_dir(); - std::ifstream input (dir + "DPP-Logo.png", std::ios::in|std::ios::binary|std::ios::ate); + std::ifstream input (dir + file, std::ios::in|std::ios::binary|std::ios::ate); if (input.is_open()) { size_t testimage_size = input.tellg(); testimage.resize(testimage_size); @@ -159,7 +159,7 @@ std::vector load_test_image() { input.close(); } else { - std::cout << "ERROR: Can't load " + dir + "DPP-Logo.png\n"; + std::cout << "ERROR: Can't load " + dir + file + "\n"; exit(1); } return testimage;