diff --git a/include/dpp/emoji.h b/include/dpp/emoji.h index 903e661764..ab95736595 100644 --- a/include/dpp/emoji.h +++ b/include/dpp/emoji.h @@ -55,38 +55,66 @@ class DPP_EXPORT emoji : public managed, public json_interface { /** * @brief Emoji name */ - std::string name; + std::string name{}; /** * @brief User id who uploaded the emoji */ - snowflake user_id; + snowflake user_id{0}; /** * @brief Flags for the emoji from dpp::emoji_flags */ - uint8_t flags; + uint8_t flags{0}; /** * @brief Image data for the emoji if uploading */ - std::string* image_data; - + std::string image_data{}; + /** * @brief Construct a new emoji object */ - emoji(); + emoji() = default; /** * @brief Construct a new emoji object with name, ID and flags - * - * @param n The emoji's name - * @param i ID, if it has one (unicode does not) - * @param f Emoji flags (emoji_flags) + * + * @param name The emoji's name + * @param id ID, if it has one (unicode does not) + * @param flags Emoji flags (emoji_flags) */ - emoji(const std::string n, const snowflake i = 0, const uint8_t f = 0); + emoji(const std::string_view name, const snowflake id = 0, const uint8_t flags = 0); + + /** + * @brief Copy constructor, copies another emoji's data + * + * @param rhs Emoji to copy + */ + emoji(const emoji &rhs) = default; + + /** + * @brief Move constructor, moves another emoji's data to this + * + * @param rhs Emoji to move from + */ + emoji(emoji &&rhs) noexcept = default; /** * @brief Destroy the emoji object */ - virtual ~emoji(); + ~emoji() override = default; + + /** + * @brief Copy assignment operator, copies another emoji's data + * + * @param rhs Emoji to copy + */ + emoji &operator=(const emoji &rhs) = default; + + /** + * @brief Move constructor, moves another emoji's data to this + * + * @param rhs Emoji to move from + */ + emoji &operator=(emoji &&rhs) noexcept = default; /** * @brief Create a mentionable emoji @@ -95,11 +123,11 @@ class DPP_EXPORT emoji : public managed, public json_interface { * @param is_animated is emoji animated. * @return std::string The formatted mention of the emoji. */ - static std::string get_mention(const std::string& name, const snowflake& id, bool is_animated = false); + static std::string get_mention(std::string_view name, snowflake id, bool is_animated = false); /** * @brief Read class values from json object - * + * * @param j A json object to read from * @return A reference to self */ @@ -107,15 +135,15 @@ class DPP_EXPORT emoji : public managed, public json_interface { /** * @brief Build the json for this object - * + * * @param with_id include the id in the JSON * @return std::string json data */ - std::string build_json(bool with_id = false) const; + std::string build_json(bool with_id = false) const override; /** * @brief Emoji requires colons - * + * * @return true Requires colons * @return false Does not require colons */ @@ -123,7 +151,7 @@ class DPP_EXPORT emoji : public managed, public json_interface { /** * @brief Emoji is managed - * + * * @return true Is managed * @return false Is not managed */ @@ -139,7 +167,7 @@ class DPP_EXPORT emoji : public managed, public json_interface { /** * @brief Is available - * + * * @return true Is available * @return false Is unavailable */ @@ -147,17 +175,17 @@ class DPP_EXPORT emoji : public managed, public json_interface { /** * @brief Load an image into the object as base64 - * + * * @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::string &image_blob, const image_type type); + emoji& load_image(std::string_view image_blob, const image_type type); /** * @brief Format to name if unicode, name:id if has id or a:name:id if animated - * + * * @return Formatted name for reactions */ std::string format() const; diff --git a/include/dpp/utility.h b/include/dpp/utility.h index cadd6c54e2..d2e98168cf 100644 --- a/include/dpp/utility.h +++ b/include/dpp/utility.h @@ -500,7 +500,7 @@ namespace dpp { * @param is_animated is emoji animated. * @return std::string The formatted mention of the emoji. */ - std::string DPP_EXPORT emoji_mention(const std::string& name, const snowflake& id, bool is_animated = false); + std::string DPP_EXPORT emoji_mention(std::string_view name, snowflake id, bool is_animated = false); /** * @brief Create a mentionable role. @@ -509,6 +509,68 @@ namespace dpp { */ std::string DPP_EXPORT role_mention(const snowflake& id); +#ifdef _DOXYGEN_ + /** + * @brief Get the mime type for an image type. + * @param type Image type + * @return std::string The mime type for this image type + */ + std::string DPP_EXPORT mime_type(image_type type); + + /** + * @brief Get the mime type for a sticker format. + * @param format Sticker format + * @return std::string The mime type for this sticker format + */ + std::string DPP_EXPORT mime_type(sticker_format format); + + /** + * @brief Get the file extension for an image type. + * @param type Image type + * @return std::string The file extension (e.g. ".png") for this image type + */ + std::string DPP_EXPORT file_extension(image_type type); + + /** + * @brief Get the file extension for a sticker format. + * @param format Sticker format + * @return std::string The file extension (e.g. ".png") for this sticker format + */ + std::string DPP_EXPORT file_extension(sticker_format format); +#else + /** + * @brief Get the mime type for an image type. + * @param type Image type + * @return std::string The mime type for this image type + */ + template + extern std::enable_if_t, std::string> DPP_EXPORT mime_type(T type); + + /** + * @brief Get the mime type for a sticker format. + * @param format Sticker format + * @return std::string The mime type for this sticker format + */ + template + extern std::enable_if_t, std::string> DPP_EXPORT mime_type(T format); + + /** + * @brief Get the file extension for an image type. + * @param type Image type + * @return std::string The file extension (e.g. ".png") for this image type + */ + template + extern std::enable_if_t, std::string> DPP_EXPORT file_extension(T type); + + /** + * @brief Get the file extension for a sticker format. + * @param format Sticker format + * @return std::string The file extension (e.g. ".png") for this sticker format + */ + template + extern std::enable_if_t, std::string> DPP_EXPORT file_extension(T format); +#endif + /** * @brief Returns the library's version string * diff --git a/src/dpp/cluster/sticker.cpp b/src/dpp/cluster/sticker.cpp index dceab66f97..050a450a45 100644 --- a/src/dpp/cluster/sticker.cpp +++ b/src/dpp/cluster/sticker.cpp @@ -23,25 +23,12 @@ namespace dpp { -namespace { - std::string get_sticker_mimetype(const sticker &s) { - static const std::map mime_types = { - { sticker_format::sf_png, "image/png" }, - { sticker_format::sf_apng, "image/png" }, - { sticker_format::sf_lottie, "application/json" }, - { sticker_format::sf_gif, "image/gif" }, - }; - - return mime_types.find(s.format_type)->second; - } -} // namespace - void cluster::guild_sticker_create(sticker &s, command_completion_event_t callback) { this->post_rest(API_PATH "/guilds", std::to_string(s.guild_id), "stickers", m_post, s.build_json(false), [this, callback](json &j, const http_request_completion_t& http) { if (callback) { callback(confirmation_callback_t(this, sticker().fill_from_json(&j), http)); } - }, s.filename, s.filecontent, get_sticker_mimetype(s)); + }, s.filename, s.filecontent, utility::mime_type(s.format_type)); } void cluster::guild_sticker_delete(snowflake sticker_id, snowflake guild_id, command_completion_event_t callback) { diff --git a/src/dpp/discordevents.cpp b/src/dpp/discordevents.cpp index ebf07753ef..1febd894fe 100644 --- a/src/dpp/discordevents.cpp +++ b/src/dpp/discordevents.cpp @@ -210,28 +210,38 @@ void set_bool_not_null(const json* j, const char *keyname, bool &v) { std::string base64_encode(unsigned char const* buf, unsigned int buffer_length) { /* Quick and dirty base64 encode */ - static const char to_base64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - size_t ret_size = buffer_length + 2; - - ret_size = 4 * ret_size / 3; - + static constexpr std::string_view to_base64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + static constexpr auto push = [](std::string &dst, unsigned char b0, unsigned char b1, unsigned char b2) { + dst.push_back(to_base64[ ((b0 & 0xfc) >> 2) ]); + dst.push_back(to_base64[ ((b0 & 0x03) << 4) + ((b1 & 0xf0) >> 4) ]); + dst.push_back(to_base64[ ((b1 & 0x0f) << 2) + ((b2 & 0xc0) >> 6) ]); + dst.push_back(to_base64[ ((b2 & 0x3f)) ]); + }; + size_t ret_size = 4 * ((buffer_length + 2) / 3); // ceil(4*size/3) + size_t i = 0; std::string ret; - ret.reserve(ret_size); - for (unsigned int i=0; i> 2) ]); - ret.push_back(to_base64[ ((b3[0] & 0x03) << 4) + ((b3[1] & 0xf0) >> 4) ]); - ret.push_back(to_base64[ ((b3[1] & 0x0f) << 2) + ((b3[2] & 0xc0) >> 6) ]); - ret.push_back(to_base64[ ((b3[2] & 0x3f)) ]); + if (buffer_length > 2) { // vvvvv avoid unsigned overflow + while (i < buffer_length - 2) { + push(ret, buf[i], buf[i + 1], buf[i + 2]); + i += 3; + } + } + size_t left = buffer_length - i; + if (left >= 1) { // handle non-multiple of 3s, pad the end with = + ret.push_back(to_base64[ ((buf[i] & 0xfc) >> 2) ]); + if (left >= 2) { + ret.push_back(to_base64[ ((buf[i] & 0x03) << 4) + ((buf[i + 1] & 0xf0) >> 4) ]); + ret.push_back(to_base64[ ((buf[i + 1] & 0x0f) << 2) ]); + ret.push_back('='); + } + else { + ret.push_back(to_base64[ ((buf[i] & 0x03) << 4) ]); + ret += "=="; + } } - return ret; } diff --git a/src/dpp/emoji.cpp b/src/dpp/emoji.cpp index c7eece3f34..78f9a9dad5 100644 --- a/src/dpp/emoji.cpp +++ b/src/dpp/emoji.cpp @@ -28,19 +28,9 @@ namespace dpp { using json = nlohmann::json; -emoji::emoji() : managed(), user_id(0), flags(0), image_data(nullptr) -{ -} - -emoji::emoji(const std::string n, const snowflake i, const uint8_t f) : managed(i), name(n), user_id(0), flags(f), image_data(nullptr) -{ -} +emoji::emoji(const std::string_view n, const snowflake i, const uint8_t f) : managed(i), name(n), flags(f) {} -emoji::~emoji() { - delete image_data; -} - -std::string emoji::get_mention(const std::string &name, const snowflake &id, bool is_animated) { +std::string emoji::get_mention(std::string_view name, snowflake id, bool is_animated) { return utility::emoji_mention(name,id,is_animated); } @@ -68,8 +58,8 @@ std::string emoji::build_json(bool with_id) const { j["id"] = std::to_string(id); } j["name"] = name; - if (image_data) { - j["image"] = *image_data; + if (!image_data.empty()) { + j["image"] = image_data; } return j.dump(); } @@ -90,21 +80,12 @@ bool emoji::is_available() const { return flags & e_available; } -emoji& emoji::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" }, - }; +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"); } - /* 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())); + image_data = "data:" + utility::mime_type(type) + ";base64," + base64_encode(reinterpret_cast(image_blob.data()), static_cast(image_blob.length())); return *this; } diff --git a/src/dpp/utility.cpp b/src/dpp/utility.cpp index bcc99ec298..a464f3ddaa 100644 --- a/src/dpp/utility.cpp +++ b/src/dpp/utility.cpp @@ -36,6 +36,7 @@ #include #include #include +#include #ifdef _WIN32 #include @@ -87,25 +88,16 @@ namespace dpp { return std::string(); } - std::string extension; - if (format == sf_png || format == sf_apng) { - extension = ".png"; - } else if (format == sf_lottie) { - extension = ".json"; - } else if (format == sf_gif) { - extension = ".gif"; - } else { - return std::string(); - } + std::string extension = file_extension(format); - return utility::cdn_host + "/stickers/" + std::to_string(sticker_id) + extension; + return extension.empty() ? std::string{} : (utility::cdn_host + "/stickers/" + std::to_string(sticker_id) + extension); } double time_f() { using namespace std::chrono; auto tp = system_clock::now() + 0ns; - return tp.time_since_epoch().count() / 1000000000.0; + return static_cast(tp.time_since_epoch().count()) / 1000000000.0; } bool has_voice() { @@ -520,15 +512,19 @@ namespace dpp { return "<#" + std::to_string(id) + ">"; } - std::string emoji_mention(const std::string &name, const snowflake &id, bool is_animated) { - auto format = [=]() { - return id ? ((is_animated ? "a:" : ":") + name + ":" + std::to_string(id)) : name; - }; - + std::string emoji_mention(std::string_view name, snowflake id, bool is_animated) { if (id) { - return "<" + format() + ">"; + std::string s{}; + + s += '<'; + s += (is_animated ? "a:" : ":"); + s += name; + s += ':'; + s += id.str(); + s += '>'; + return s; } else { - return ":" + format() + ":"; + return ":" + std::string{name} + ":"; } } @@ -536,6 +532,107 @@ namespace dpp { return "<@&" + std::to_string(id) + ">"; } + template + std::enable_if_t, std::string> mime_type(T type) { + static constexpr auto get_image_mime = [](image_type t) constexpr noexcept { + using namespace std::string_view_literals; + + switch (t) { + case i_png: + return "image/png"sv; + + case i_jpg: + return "image/jpeg"sv; + + case i_gif: + return "image/gif"sv; + + case i_webp: + return "image/webp"sv; + } + return std::string_view{}; // unknown + }; + + return std::string{get_image_mime(type)}; + } + + // Explicit instantiation, shoves it into the DLL + template std::string mime_type(image_type t); + + template + std::enable_if_t, std::string> mime_type(T format) { + static constexpr auto get_sticker_mime = [](sticker_format f) constexpr noexcept { + using namespace std::string_view_literals; + + switch (f) { + case sf_png: + return "image/png"sv; + + case sf_apng: + return "image/apng"sv; + + case sf_lottie: + return "application/json"sv; + + case sf_gif: + return "image/gif"sv; + } + return std::string_view{}; // unknown + }; + return std::string{get_sticker_mime(format)}; + } + + template std::string mime_type(sticker_format t); + + template + std::enable_if_t, std::string> file_extension(T type) { + static constexpr auto get_image_ext = [](image_type t) constexpr noexcept { + using namespace std::string_view_literals; + + switch (t) { + case i_png: + return ".png"sv; + + case i_jpg: + return ".jpg"sv; + + case i_gif: + return ".gif"sv; + + case i_webp: + return ".webp"sv; + } + return std::string_view{}; // unknown + }; + + return std::string{get_image_ext(type)}; + } + + template std::string file_extension(image_type t); + + template + std::enable_if_t, std::string> file_extension(T format) { + static constexpr auto get_sticker_ext = [](sticker_format f) constexpr noexcept { + using namespace std::string_view_literals; + + switch (f) { + case sf_png: + case sf_apng: + return ".png"sv; + + case sf_lottie: + return ".json"sv; + + case sf_gif: + return ".gif"sv; + } + return std::string_view{}; // unknown + }; + return std::string{get_sticker_ext(format)}; + } + + template std::string file_extension(sticker_format t); + std::string make_url_parameters(const std::map& parameters) { std::string output; for(auto& [k, v] : parameters) { diff --git a/src/unittest/test.cpp b/src/unittest/test.cpp index 3ad4b56288..7b0df49fea 100644 --- a/src/unittest/test.cpp +++ b/src/unittest/test.cpp @@ -89,6 +89,17 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b 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); @@ -1645,7 +1656,7 @@ Markdown lol \\|\\|spoiler\\|\\| \\~\\~strikethrough\\~\\~ \\`small \\*code\\* b 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()) { - const auto &thread = event.get(); + [[maybe_unused]] const auto &thread = event.get(); set_test("THREAD_CREATE", true); } // the thread tests are in the on_thread_create event handler diff --git a/src/unittest/unittest.cpp b/src/unittest/unittest.cpp index 971f6a5d2d..bfbbcec7bc 100644 --- a/src/unittest/unittest.cpp +++ b/src/unittest/unittest.cpp @@ -83,6 +83,7 @@ std::map tests = { {"MD_ESC_1", {tt_offline, "Markdown escaping (ignore code block contents)", false, false}}, {"MD_ESC_2", {tt_offline, "Markdown escaping (escape code block contents)", false, false}}, {"URLENC", {tt_offline, "URL encoding", false, false}}, + {"BASE64ENC", {tt_offline, "Base64 encoding", false, false}}, {"SYNC", {tt_online, "sync()", false, false}}, {"COMPARISON", {tt_offline, "manged object comparison", false, false}}, {"CHANNELCACHE", {tt_online, "find_channel()", false, false}},