diff --git a/docpages/example_code/components_v2.cpp b/docpages/example_code/components_v2.cpp new file mode 100644 index 0000000000..f7dc7c3fb6 --- /dev/null +++ b/docpages/example_code/components_v2.cpp @@ -0,0 +1,71 @@ +#include + +int main() { + dpp::cluster bot("token"); + + bot.on_log(dpp::utility::cout_logger()); + + bot.on_ready([&bot](const auto& event) { + if (dpp::run_once()) { + bot.global_bulk_command_create({ dpp::slashcommand("cats", "I love cats", bot.me.id) }); + } + }); + + bot.on_button_click([](const dpp::button_click_t& event) { + event.reply("You declared your love for cats by clicking button id: " + event.custom_id); + }); + + /* This is a detailed example of using many different types of component. For a complete + * list of supported components, see the Discord developer documentation and the definition + * of dpp::component_type. + */ + bot.register_command("cats", [](const dpp::slashcommand_t& e) { + e.reply(dpp::message() + /* Remember to set the message flag for components v2 */ + .set_flags(dpp::m_using_components_v2).add_component_v2( + /* Reply with a container... */ + dpp::component() + .set_type(dpp::cot_container) + .set_accent(dpp::utility::rgb(255, 0, 0)) + .set_spoiler(true) + .add_component_v2( + /* ...which contains a section... */ + dpp::component() + .set_type(dpp::cot_section) + .add_component_v2( + /* ...with text... */ + dpp::component() + .set_type(dpp::cot_text_display) + .set_content("Click if you love cats") + ) + .set_accessory( + /* ...and an accessory button to the right */ + dpp::component() + .set_type(dpp::cot_button) + .set_label("Click me") + .set_style(dpp::cos_danger) + .set_id("button") + ) + ) + ).add_component_v2( + /* ... with a large visible divider between... */ + dpp::component() + .set_type(dpp::cot_separator) + .set_spacing(dpp::sep_large) + .set_divider(true) + ).add_component_v2( + /* ... followed by a media gallery... */ + dpp::component() + .set_type(dpp::cot_media_gallery) + .add_media_gallery_item( + /* ...containing one cat pic (obviously) */ + dpp::component() + .set_type(dpp::cot_thumbnail) + .set_description("A cat") + .set_thumbnail("https://www.catster.com/wp-content/uploads/2023/11/Beluga-Cat-e1714190563227.webp") + ) + )); + }); + + bot.start(dpp::st_wait); +} diff --git a/docpages/example_programs/interactions_and_components/components-menu.md b/docpages/example_programs/interactions_and_components/components-menu.md index 5c12147fa8..d0dece19fd 100644 --- a/docpages/example_programs/interactions_and_components/components-menu.md +++ b/docpages/example_programs/interactions_and_components/components-menu.md @@ -7,3 +7,4 @@ Components are anything that can be attached to a message or a \ref modal-dialog * \subpage components3 * \subpage default_select_value * \subpage editing_message_after_click +* \subpage components_v2 diff --git a/docpages/example_programs/interactions_and_components/components/components_v2.md b/docpages/example_programs/interactions_and_components/components/components_v2.md new file mode 100644 index 0000000000..43960b1e6a --- /dev/null +++ b/docpages/example_programs/interactions_and_components/components/components_v2.md @@ -0,0 +1,28 @@ +\page components_v2 Components V2 + +From March 2025 onwards Discord have released a **new way to handle components** in a Discord application/bot. The previous methods of working with components remain, and are accessible without +any changes in D++. If you want to use the new style of components you may do so, which gives far greater control over how message containing images, buttons, sections etc are formatted. + +Components are attached to any message or interaction reply, via the dpp::message::add_component() or dpp::message::add_component_v2() function. You must also be sure to set the flag +dpp::m_using_components_v2 on the message to allow the new features. + +When using components v2, the following limits apply which do not apply with components v1 or traditional embeds: + +* Setting the message `content` or `embeds` will not be allowed (components v2 replaces the functionality) +* You can have a maximum of 10 top level components per message. The maximum number of nested components is 30. +* Audio files are not supported at present +* Text preview for text/plain files is not supported +* URLs will not have embeds generated for them +* The total length of the entire message, including all components within and the content of those components, is 4000 UTF-8 characters. + +Here is a detailed example of how to create components v2 replies. + +\warning Please note that where you would use add_component() previously, you **should** use add_component_v2() (on component or message objects). This is because the v2 version will not automatically add action rows, which is a limitation which has been removed in version 2 but is still required for version 1. + +\include{cpp} components_v2.cpp + +There are many new component types, for a complete list see the definition of dpp::component_type + +If you run the example program above, you will be shown a message containing your components: + +\image html components.gif diff --git a/docpages/images/components.gif b/docpages/images/components.gif new file mode 100755 index 0000000000..210e2e9f72 Binary files /dev/null and b/docpages/images/components.gif differ diff --git a/include/dpp/message.h b/include/dpp/message.h index 04c81f64c6..46b6435243 100644 --- a/include/dpp/message.h +++ b/include/dpp/message.h @@ -76,6 +76,53 @@ enum component_type : uint8_t { * @brief Select menu for channels. */ cot_channel_selectmenu = 8, + + /** + * @brief Section + * @note Available in components v2 only + */ + cot_section = 9, + + /** + * @brief Simple text + * @note Available in components v2 only + */ + cot_text_display = 10, + + /** + * @brief Image thumbnail, clickable to expand + * @note Available in components v2 only + */ + cot_thumbnail = 11, + + /** + * @brief Collection of media (images, videos) + * @note Available in components v2 only + */ + cot_media_gallery = 12, + + /** + * @brief File attachment from the uploads of the message + * @note Available in components v2 only + */ + cot_file = 13, + + /** + * @brief Separator between sections or other components + * @note Available in components v2 only + */ + cot_separator = 14, + + /** + * @brief Content inventory entry + */ + cot_content_inventory_entry = 16, + + /** + * @brief Container for other components + * @note Available in components v2 only + */ + cot_container = 17, }; /** @@ -333,6 +380,98 @@ struct DPP_EXPORT select_option : public json_interface { select_option& set_animated(bool anim); }; +/** + * @brief Loading state for "unfurled" media, e.g. thumbnails and images in a message or component + */ +enum media_loading_state : uint8_t { + + /** + * @brief Loading state is unknown + */ + ls_unknown = 0, + + /** + * @brief Discord is loading the media + */ + ls_loading = 1, + + /** + * @brief The image was processed and loaded successfully + */ + ls_loaded_success = 2, + + /** + * @brief The media was not found + */ + ls_not_found = 3, +}; + +/** + * @brief An video, image or thumbnail in a dpp::embed or dpp::component (v2) + */ +struct DPP_EXPORT embed_image { + /** + * @brief URL to image or video. + */ + std::string url; + + /** + * @brief Proxied image url. + */ + std::string proxy_url; + + /** + * @brief Height (calculated by discord). + */ + uint32_t height{0}; + + /** + * @brief Width (calculated by discord). + */ + uint32_t width{0}; + + /** + * @brief Media loading state + */ + media_loading_state loading_state{ls_unknown}; + + /** + * @brief placeholder + */ + std::string placeholder; + + /** + * @brief Placeholder version + */ + uint8_t placeholder_version{1}; + + /** + * @brief Content type + */ + std::string content_type; + + /** + * @brief Flags (documented as present, but contents not documented) + */ + uint32_t flags{0}; +}; + +/** + * @brief Spacing types for components v2 separator + */ +enum separator_spacing : uint8_t { + + /** + * @brief Small separator + */ + sep_small = 1, + + /** + * @brief Large separator + */ + sep_large = 2, +}; + /** * @brief Represents the component object. * A component is a clickable button or drop down list @@ -445,7 +584,8 @@ class DPP_EXPORT component : public json_interface { * * @note The amount of default values must be in the range defined by dpp::component::min_values and dpp::component::max_values. * - * @warning Only available for auto-populated select menu components, which include dpp::cot_user_selectmenu, dpp::cot_role_selectmenu, dpp::cot_mentionable_selectmenu, and dpp::cot_channel_selectmenu components. + * @warning Only available for auto-populated select menu components, which include dpp::cot_user_selectmenu, dpp::cot_role_selectmenu, + * * dpp::cot_mentionable_selectmenu, and dpp::cot_channel_selectmenu components. */ std::vector default_values; @@ -470,6 +610,58 @@ class DPP_EXPORT component : public json_interface { */ partial_emoji emoji; + /** + * @brief Text content for section and text display types (v2) + */ + std::string content; + + /** + * @brief accessory component for components which support it (v2) + * Can be a button or a thumbnail. + */ + std::shared_ptr accessory; + + /** + * @brief Description for media items (v2) + */ + std::string description; + + /** + * @brief Spoiler state for media and file items + */ + bool spoiler; + + /** + * @brief can be set for separator types (v2) + */ + bool is_divider; + + /** + * @brief Valid for separator types (v2) + */ + separator_spacing spacing; + + /** + * @brief Unfurled media for thumbnail objects (v2) + */ + std::optional thumbnail; + + /** + * @brief Unfurled file URL for file objects (v2) + * Should be in the format "attachment://..." + */ + std::optional file; + + /** + * @brief Media gallery items for media galleries (v2) + */ + std::vector> media_gallery_items; + + /** + * @brief Accent colour for container types (v2) + */ + std::optional accent; + /** * @brief Constructor */ @@ -488,6 +680,83 @@ class DPP_EXPORT component : public json_interface { */ component& add_channel_type(uint8_t ct); + /** + * @brief For text content types, set content + * @note Ignored for other types + * @param text text to set + * @return + */ + component& set_content(const std::string& text); + + /** + * @brief Set divider visibility for separator type (v2) + * @param divider true to show a visible divider + * @return self + */ + component& set_divider(bool divider); + + /** + * @brief Set accent colour for container type (v2) + * @param accent_colour Accent colour for container + * @return self + */ + component& set_accent(uint32_t accent_colour); + + /** + * @brief Set separator spacing for separator type (v2) + * @param sep_spacing separator spacing, small or large + * @return self + */ + component& set_spacing(separator_spacing sep_spacing); + + /** + * @brief Set the attachment url for file type components. + * The format of the attachment url should be of the type + * "attachment://...". + * @param attachment_url url to attachment. Should have been + * attached via add_file(). + * @return self + */ + component& set_file(std::string_view attachment_url); + + /** + * @brief Add a media gallery item to a media gallery type (v2) + * @param media_gallery_item item to add + * @return + */ + component& add_media_gallery_item(const component& media_gallery_item); + + /** + * @brief For media types, set description + * @note Ignored for other types + * @param text text to set + * @return + */ + component& set_description(const std::string& text); + + /** + * @brief For media and file component types, set spoiler status + * @note Ignored for other types + * @param spoiler_enable True to enable spoiler masking + * @return + */ + component& set_spoiler(bool spoiler_enable); + + /** + * @brief Set component thumbnail for thumbnail type (v2. + * @param url The embed thumbnail url (only supports http(s) and attachments) + * @return A reference to self so this method may be "chained". + */ + component& set_thumbnail(std::string_view url); + + /** + * @brief Set accessory component for components which support it. + * Can be a button or a thumbnail image. + * @param accessory_component accessory component + * @return + */ + component& set_accessory(const component& accessory_component); + /** * @brief Set the type of the component. Button components * (type dpp::cot_button) should always be contained within @@ -647,11 +916,26 @@ class DPP_EXPORT component : public json_interface { * Adding subcomponents to a component will automatically * set this component's type to dpp::cot_action_row. * + * @note Automatic creation of action rows is retained for backwards + * compatibility with components v1. If you want to add components v2, + * and do not want automatic creation of action rows, use + * dpp::component::add_component_v2() instead. + * * @param c The sub-component to add * @return component& reference to self */ component& add_component(const component& c); + /** + * @brief Add a sub-component, does not automatically create + * action rows. This should be used for components v2 where + * you do not want to add action rows automatically. + * + * @param c The sub-component to add + * @return component& reference to self + */ + component& add_component_v2(const component& c); + /** * @brief Add a default value. * @@ -726,31 +1010,6 @@ struct DPP_EXPORT embed_footer { embed_footer& set_proxy(std::string_view p); }; -/** - * @brief An video, image or thumbnail in a dpp::embed - */ -struct DPP_EXPORT embed_image { - /** - * @brief URL to image or video. - */ - std::string url; - - /** - * @brief Proxied image url. - */ - std::string proxy_url; - - /** - * @brief Height (calculated by discord). - */ - std::string height; - - /** - * @brief Width (calculated by discord). - */ - std::string width; -}; - /** * @brief Embed provider in a dpp::embed. Received from discord but cannot be sent */ @@ -1719,6 +1978,16 @@ enum message_flags : uint16_t { * @brief This message is a voice message. */ m_is_voice_message = 1 << 13, + + /** + * @brief Contains forwarded snapshot + */ + m_has_snapshot = 1 << 14, + + /** + * @brief Message components vector contains v2 components + */ + m_using_components_v2 = 1 << 15, }; /** @@ -1767,7 +2036,7 @@ namespace embed_type { /** * @brief Message types for dpp::message::type */ -enum message_type { +enum message_type : uint8_t { /** * @brief Default */ @@ -2533,13 +2802,47 @@ struct DPP_EXPORT message : public managed, json_interface { bool is_voice_message() const; /** - * @brief Add a component (button) to message + * @brief True if the message has a snapshot + * + * @return True if voice message + */ + bool has_snapshot() const; + + /** + * @brief True if the message is using components v2 + * + * @return True if voice message + */ + bool is_using_components_v2() const; + + /** + * @brief Add a component to a message + * + * @note If the component type you add is only available in + * components v2, this method will automatically add the + * m_using_components_v2 flag to your message. * * @param c component to add * @return message& reference to self */ message& add_component(const component& c); + /** + * @brief Add a component to a message + * + * @note If the component type you add is only available in + * components v2, this method will automatically add the + * m_using_components_v2 flag to your message. + * + * This is an alias of add_component() for readability when + * using components v2, so you can use add_component_v2() + * everywhere. It does exactly the same as add_component(). + * + * @param c component to add + * @return message& reference to self + */ + message& add_component_v2(const component& c); + /** * @brief Add an embed to message * diff --git a/src/dpp/cluster/webhook.cpp b/src/dpp/cluster/webhook.cpp index 8ebf9370e4..577cce35d8 100644 --- a/src/dpp/cluster/webhook.cpp +++ b/src/dpp/cluster/webhook.cpp @@ -70,6 +70,7 @@ void cluster::execute_webhook(const class webhook &wh, const struct message& m, std::string parameters = utility::make_url_parameters({ {"wait", wait}, {"thread_id", thread_id}, + {"with_components", true}, }); std::string body; if (!thread_name.empty() || !wh.avatar.to_string().empty() || !wh.name.empty()) { // only use json::parse if thread_name is set diff --git a/src/dpp/message.cpp b/src/dpp/message.cpp index 9afa80009a..42e0ad123b 100644 --- a/src/dpp/message.cpp +++ b/src/dpp/message.cpp @@ -19,6 +19,7 @@ * ************************************************************************************/ #include +#include #include #include #include @@ -29,9 +30,20 @@ namespace dpp { using json = nlohmann::json; +std::set components_v2_only_types = { + cot_section, + cot_text_display, + cot_thumbnail, + cot_media_gallery, + cot_file, + cot_separator, + cot_container +}; + component::component() : type(cot_action_row), label(""), style(cos_primary), custom_id(""), - min_values(-1), max_values(-1), min_length(0), max_length(0), disabled(false), required(false) + min_values(-1), max_values(-1), min_length(0), max_length(0), disabled(false), + required(false), spoiler(false), is_divider(false), spacing(sep_small) { emoji.animated = false; emoji.id = 0; @@ -39,12 +51,115 @@ component::component() : } +component& component::set_content(const std::string& text) { + content = text; + return *this; +} + +component& component::set_description(const std::string& text) { + description = text; + return *this; +} + +component& component::set_divider(bool divider) { + is_divider = divider; + return *this; +} + +component& component::set_spacing(separator_spacing sep_spacing) { + spacing = sep_spacing; + return *this; +} + +component& component::set_spoiler(bool spoiler_enable) { + spoiler = spoiler_enable; + return *this; +} + +component& component::set_accessory(const component& accessory_component) { + if (accessory_component.type != cot_thumbnail && accessory_component.type != cot_button) { + throw logic_exception("Accessories can only be buttons or thumbnails"); + } + accessory = std::make_shared(accessory_component); + return *this; +} + +component& component::set_thumbnail(std::string_view url) { + dpp::embed_image t; + t.url = url; + thumbnail = t; + return *this; +} + +component& component::set_file(std::string_view attachment_url) { + dpp::embed_image t; + t.url = attachment_url; + file = t; + return *this; +} + +component& component::set_accent(uint32_t accent_colour) { + accent = accent_colour; + return *this; +} + +component& component::add_media_gallery_item(const component& media_gallery_item) { + media_gallery_items.emplace_back(std::make_shared(media_gallery_item)); + return *this; +} + + component& component::fill_from_json_impl(nlohmann::json* j) { type = static_cast(int8_not_null(j, "type")); label = string_not_null(j, "label"); custom_id = string_not_null(j, "custom_id"); disabled = bool_not_null(j, "disabled"); placeholder = string_not_null(j, "placeholder"); + description = string_not_null(j, "description"); + spoiler = bool_not_null(j, "spoiler"); + is_divider = bool_not_null(j, "divider"); + spacing = static_cast(int8_not_null(j, "spacing")); + if (j->contains("accent_color")) { + accent = static_cast(int32_not_null(j, "accent_color")); + } + content = string_not_null(j, "content"); + if (j->contains("items") && type == cot_media_gallery) { + for (auto& item : j->at("items")) { + add_media_gallery_item(fill_from_json_impl(&item)); + } + } + if (j->contains("thumbnail")) { + dpp::embed_image t; + json& fi = (*j)["thumbnail"]; + t.url = string_not_null(&fi, "url"); + t.height = int32_not_null(&fi, "height"); + t.width = int32_not_null(&fi, "width"); + t.proxy_url = string_not_null(&fi, "proxy_url"); + t.loading_state = static_cast(int8_not_null(&fi, "loading_state")); + t.content_type = string_not_null(&fi, "content_type"); + t.placeholder = string_not_null(&fi, "placeholder"); + t.placeholder_version = int8_not_null(&fi, "placeholder_version"); + t.flags = int32_not_null(&fi, "flags"); + thumbnail = t; + } + if (j->contains("file")) { + dpp::embed_image t; + json& fi = (*j)["file"]; + t.url = string_not_null(&fi, "url"); + t.height = int32_not_null(&fi, "height"); + t.width = int32_not_null(&fi, "width"); + t.proxy_url = string_not_null(&fi, "proxy_url"); + t.loading_state = static_cast(int8_not_null(&fi, "loading_state")); + t.content_type = string_not_null(&fi, "content_type"); + t.placeholder = string_not_null(&fi, "placeholder"); + t.placeholder_version = int8_not_null(&fi, "placeholder_version"); + t.flags = int32_not_null(&fi, "flags"); + file = t; + } + + if (j->contains("accessory")) { + set_accessory(fill_from_json_impl(&(j->at("accessory")))); + } if (j->contains("min_values") && j->at("min_values").is_number_integer()) { min_values = j->at("min_values").get(); } @@ -59,7 +174,7 @@ component& component::fill_from_json_impl(nlohmann::json* j) { default_values.push_back(d); } } - if (type == cot_action_row) { + if (j->contains("components") && j->at("components").is_array()) { set_object_array_not_null(j, "components", components); } else if (type == cot_button) { // button specific fields style = static_cast(int8_not_null(j, "style")); @@ -108,6 +223,12 @@ component& component::add_component(const component& c) return *this; } +component& component::add_component_v2(const component& c) +{ + components.emplace_back(c); + return *this; +} + component& component::add_channel_type(uint8_t ct) { if (type == cot_action_row) { set_type(cot_channel_selectmenu); @@ -252,6 +373,48 @@ void to_json(json& j, const attachment& a) { void to_json(json& j, const component& cp) { j["type"] = cp.type; + if (cp.accessory) { + j["accessory"] = *cp.accessory; + } + if (!cp.content.empty() && cp.type == cot_text_display) { + j["content"] = cp.content; + } + if (cp.type == cot_thumbnail) { + if (!cp.description.empty()) { + j["description"] = cp.description; + } + j["spoiler"] = cp.spoiler; + if (cp.thumbnail.has_value()) { + j["media"] = { + {"url", cp.thumbnail->url} + }; + } + } + if (cp.type == cot_container) { + j["spoiler"] = cp.spoiler; + if (cp.accent.has_value()) { + j["accent_color"] = cp.accent.value(); + } + } + if (cp.type == cot_separator) { + j["divider"] = cp.is_divider; + j["spacing"] = static_cast(cp.spacing); + } + if (cp.type == cot_file) { + if (cp.file.has_value()) { + j["media"] = { + {"url", cp.file->url} + }; + } + j["spoiler"] = cp.spoiler; + } + if (cp.type == cot_media_gallery) { + j["items"] = json::array(); + auto& item_array = j["items"]; + for (auto& item : cp.media_gallery_items) { + item_array.push_back(*item); + } + } if (cp.type == cot_text) { j["label"] = cp.label; j["required"] = cp.required; @@ -710,12 +873,22 @@ message::message(snowflake _channel_id, std::string_view _content, message_type } message& message::add_component(const component& c) { + if (components_v2_only_types.find(c.type) != components_v2_only_types.end()) { + set_flags(flags | m_using_components_v2); + } components.emplace_back(c); return *this; } +message& message::add_component_v2(const component& c) { + /* This is just an alias for readability in apps */ + return add_component(c); +} + message& message::add_embed(const embed& e) { - embeds.emplace_back(e); + if ((flags & m_using_components_v2) == 0) { + embeds.emplace_back(e); + } return *this; } @@ -810,7 +983,9 @@ message::message(std::string_view _content, message_type t) : message() { } message::message(const embed& _embed) : message() { - embeds.emplace_back(_embed); + if ((flags & m_using_components_v2) == 0) { + embeds.emplace_back(_embed); + } } message::message(snowflake _channel_id, const embed& _embed) : message(_embed) { @@ -840,9 +1015,14 @@ embed::embed(json* j) : embed() { embed_image curr; json& fi = (*j)[s]; curr.url = string_not_null(&fi, "url"); - curr.height = string_not_null(&fi, "height"); - curr.width = string_not_null(&fi, "width"); + curr.height = int32_not_null(&fi, "height"); + curr.width = int32_not_null(&fi, "width"); curr.proxy_url = string_not_null(&fi, "proxy_url"); + curr.loading_state = static_cast(int8_not_null(&fi, "loading_state")); + curr.content_type = string_not_null(&fi, "content_type"); + curr.placeholder = string_not_null(&fi, "placeholder"); + curr.placeholder_version = int8_not_null(&fi, "placeholder_version"); + curr.flags = int32_not_null(&fi, "flags"); if (s == "image") { image = curr; } else if (s == "video") { @@ -1106,6 +1286,21 @@ time_t attachment::get_issued_time() const { return std::stol(is_attr->substr(3), nullptr, 16); } +static void recurse_components(json& j, const std::vector& components) { + if (components.empty()) { + return; + } + j["components"] = json::array(); + for (auto & component : components) { + json n; + to_json(n, component); + if (!component.components.empty()) { + recurse_components(n, component.components); + } + j["components"].push_back(n); + } +} + json message::to_json(bool with_id, bool is_interaction_response) const { /* This is the basics. once it works, expand on it. */ json j({ @@ -1114,9 +1309,12 @@ json message::to_json(bool with_id, bool is_interaction_response) const { {"nonce", nonce}, {"flags", flags}, {"type", type}, - {"content", content} }); + if ((flags & m_using_components_v2) == 0) { + j["content"] = content; + } + if (with_id) { j["id"] = std::to_string(id); } @@ -1180,16 +1378,20 @@ json message::to_json(bool with_id, bool is_interaction_response) const { } } - j["components"] = json::array(); - for (auto & component : components) { - json n; - n["type"] = cot_action_row; - n["components"] = {}; - for (auto & subcomponent : component.components) { - json sn = subcomponent; - n["components"].push_back(sn); + if ((flags & m_using_components_v2) == 0) { + j["components"] = json::array(); + for (auto & component : components) { + json n; + n["type"] = cot_action_row; + n["components"] = {}; + for (auto & subcomponent : component.components) { + json sn = subcomponent; + n["components"].push_back(sn); + } + j["components"].push_back(n); } - j["components"].push_back(n); + } else { + recurse_components(j, components); } j["attachments"] = json::array(); @@ -1198,48 +1400,50 @@ json message::to_json(bool with_id, bool is_interaction_response) const { j["attachments"].push_back(a); } - j["embeds"] = json::array(); - for (auto& embed : embeds) { - json e; - if (!embed.description.empty()) { - e["description"] = embed.description; - } - if (!embed.title.empty()) { - e["title"] = embed.title; - } - if (!embed.url.empty()) { - e["url"] = embed.url; - } - 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; - } - if (embed.image.has_value()) { - e["image"]["url"] = embed.image->url; - } - if (embed.thumbnail.has_value()) { - e["thumbnail"]["url"] = embed.thumbnail->url; - } - if (embed.author.has_value()) { - e["author"]["name"] = embed.author->name; - e["author"]["url"] = embed.author->url; - e["author"]["icon_url"] = embed.author->icon_url; - } - if (embed.fields.size()) { - e["fields"] = json(); - for (auto& field : embed.fields) { - json f({ {"name", field.name}, {"value", field.value}, {"inline", field.is_inline} }); - e["fields"].push_back(f); + if ((flags & m_using_components_v2) == 0) { + j["embeds"] = json::array(); + for (auto& embed : embeds) { + json e; + if (!embed.description.empty()) { + e["description"] = embed.description; + } + if (!embed.title.empty()) { + e["title"] = embed.title; + } + if (!embed.url.empty()) { + e["url"] = embed.url; + } + 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; + } + if (embed.image.has_value()) { + e["image"]["url"] = embed.image->url; + } + if (embed.thumbnail.has_value()) { + e["thumbnail"]["url"] = embed.thumbnail->url; + } + if (embed.author.has_value()) { + e["author"]["name"] = embed.author->name; + e["author"]["url"] = embed.author->url; + e["author"]["icon_url"] = embed.author->icon_url; + } + if (embed.fields.size()) { + e["fields"] = json(); + for (auto& field : embed.fields) { + json f({ {"name", field.name}, {"value", field.value}, {"inline", field.is_inline} }); + e["fields"].push_back(f); + } + } + if (embed.timestamp) { + e["timestamp"] = ts_to_string(embed.timestamp); } - } - if (embed.timestamp) { - e["timestamp"] = ts_to_string(embed.timestamp); - } - j["embeds"].push_back(e); + j["embeds"].push_back(e); + } } if (attached_poll.has_value()) { @@ -1306,6 +1510,13 @@ bool message::is_voice_message() const { return flags & m_is_voice_message; } +bool message::has_snapshot() const { + return flags & m_has_snapshot; +} + +bool message::is_using_components_v2() const { + return flags & m_using_components_v2; +} message& message::fill_from_json(json* d, cache_policy_t cp) { this->id = snowflake_not_null(d, "id"); diff --git a/src/userapptest/userapp.cpp b/src/userapptest/userapp.cpp index 99f75e68fd..5f3682c03f 100644 --- a/src/userapptest/userapp.cpp +++ b/src/userapptest/userapp.cpp @@ -41,20 +41,59 @@ int main() { * This is a user-app command which can be executed anywhere and is added to the user's profile. */ bot.global_bulk_command_create({ - dpp::slashcommand("userapp", "Test user app command", bot.me.id) + dpp::slashcommand("userapp", "Test command", bot.me.id) .set_interaction_contexts({dpp::itc_guild, dpp::itc_bot_dm, dpp::itc_private_channel}) }); } }); + bot.on_button_click([](const dpp::button_click_t& event) { + event.reply("You declared your love for cats by clicking button id: " + event.custom_id); + }); + bot.register_command("userapp", [](const dpp::slashcommand_t& e) { - /** - * Simple test output that shows the context of the command - */ - e.reply("This is the `/userapp` command." + std::string( - e.command.is_user_app_interaction() ? - " Executing as a user interaction owned by user: <@" + e.command.get_authorizing_integration_owner(dpp::ait_user_install).str() + ">" : - " Executing as a guild interaction on guild id " + e.command.guild_id.str() + e.reply(dpp::message().set_flags(dpp::m_using_components_v2).add_component_v2( + /* A container... */ + dpp::component() + .set_type(dpp::cot_container) + .set_accent(dpp::utility::rgb(255, 0, 0)) + .set_spoiler(true) + .add_component_v2( + /* ...which contains a section... */ + dpp::component() + .set_type(dpp::cot_section) + .add_component_v2( + /* ...with text... */ + dpp::component() + .set_type(dpp::cot_text_display) + .set_content("Click if you love cats") + ) + .set_accessory( + /* ...and an accessory button to the right */ + dpp::component() + .set_type(dpp::cot_button) + .set_label("Click me") + .set_style(dpp::cos_danger) + .set_id("button") + ) + ) + ).add_component_v2( + /* ... with a large visible divider between... */ + dpp::component() + .set_type(dpp::cot_separator) + .set_spacing(dpp::sep_large) + .set_divider(true) + ).add_component_v2( + /* ... followed by a media gallery... */ + dpp::component() + .set_type(dpp::cot_media_gallery) + .add_media_gallery_item( + /* ...containing one cat pic (obviously) */ + dpp::component() + .set_type(dpp::cot_thumbnail) + .set_description("A cat") + .set_thumbnail("https://www.catster.com/wp-content/uploads/2023/11/Beluga-Cat-e1714190563227.webp") + ) )); });