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] 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;