diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 9d04100c6c..e963031237 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,7 +10,7 @@ updates: - package-ecosystem: "docker" directory: "/" schedule: - interval: "daily" + interval: "monthly" target-branch: "dev" - package-ecosystem: github-actions diff --git a/doxygen-awesome-css b/doxygen-awesome-css index af1d9030b3..4593c19803 160000 --- a/doxygen-awesome-css +++ b/doxygen-awesome-css @@ -1 +1 @@ -Subproject commit af1d9030b3ffa7b483fa9997a7272fb12af6af4c +Subproject commit 4593c198030b6e5019aae4d9c2eeb7497f02dbf8 diff --git a/include/dpp/appcommand.h b/include/dpp/appcommand.h index a4f99f7938..651c6b6ead 100644 --- a/include/dpp/appcommand.h +++ b/include/dpp/appcommand.h @@ -19,6 +19,7 @@ * ************************************************************************************/ #pragma once +#include #include #include #include @@ -773,6 +774,26 @@ enum interaction_type { it_modal_submit = 5, }; +/* +* @brief Context type where the interaction can be used or triggered from, e.g. guild, user etc +*/ +enum interaction_context_type { + /** + * @brief Interaction can be used within servers + */ + itc_guild = 0, + + /** + * @brief Interaction can be used within DMs with the app's bot user + */ + itc_bot_dm = 1, + + /** + * @brief Interaction can be used within Group DMs and DMs other than the app's bot user + */ + itc_private_channel = 2, +}; + /** * @brief Right-click context menu types */ @@ -952,6 +973,16 @@ class DPP_EXPORT interaction : public managed, public json_interface authorizing_integration_owners; + + /** + * @brief Context where the interaction was triggered from + */ + std::optional context; + /** * @brief ID of the application this interaction is for. */ @@ -1194,6 +1225,30 @@ class DPP_EXPORT interaction : public managed, public json_interface integration_types; + + /** + * @brief Interaction context(s) where the command can be used, only for globally-scoped commands. By default, all interaction context types included for new commands. + */ + std::vector contexts; + /** * @brief True if this command should be allowed in a DM * D++ defaults this to false. Cannot be set to true in a guild * command, only a global command. + * @deprecated Use dpp::slashcommand_t::set_interaction_contexts instead */ bool dm_permission; @@ -1543,6 +1609,14 @@ class DPP_EXPORT slashcommand : public managed, public json_interface contexts); + /** * @brief Adds a permission to the command * diff --git a/include/dpp/application.h b/include/dpp/application.h index 5fae5738d5..88c50dba23 100644 --- a/include/dpp/application.h +++ b/include/dpp/application.h @@ -21,6 +21,7 @@ ************************************************************************************/ #pragma once +#include #include #include #include @@ -30,6 +31,8 @@ #include #include #include +#include +#include namespace dpp { @@ -209,6 +212,13 @@ class DPP_EXPORT app_team { snowflake owner_user_id; }; +/** + * @brief Configuration object for an app installation + */ +struct DPP_EXPORT integration_configuration { + std::optional oauth2_install_params; +}; + /** * @brief The application class represents details of a bot application */ @@ -357,6 +367,11 @@ class DPP_EXPORT application : public managed, public json_interface integration_types_config; + /** * @brief The application's default custom authorization link, if enabled. */ diff --git a/include/dpp/integration.h b/include/dpp/integration.h index 1dc78747fc..ce806c0ca4 100644 --- a/include/dpp/integration.h +++ b/include/dpp/integration.h @@ -30,6 +30,20 @@ namespace dpp { +/** + * @brief Where an app can be installed, also called its supported installation contexts. + */ +enum application_integration_types { + /** + * @brief Installable to servers + */ + ait_guild_install = 0, + /** + * @brief Installable to users + */ + ait_user_install = 1, +}; + /** * @brief Integration types */ diff --git a/include/dpp/message.h b/include/dpp/message.h index 200f3f7a07..04c81f64c6 100644 --- a/include/dpp/message.h +++ b/include/dpp/message.h @@ -2010,6 +2010,43 @@ namespace cache_policy { }; +/** + * @brief Metadata about the interaction, including the source of the interaction and relevant server and user IDs. + */ +struct DPP_EXPORT interaction_metadata_type { + + /** + * @brief ID of the interaction + */ + snowflake id; + + /** + * @brief User who triggered the interaction + */ + uint8_t type; + + /** + * @brief User who triggered the interaction + */ + user usr; + + /** + * @brief ID of the original response message, present only on follow-up messages + */ + snowflake original_response_message_id; + + /** + * @brief ID of the message that contained interactive component, present only on messages created from component interactions + */ + snowflake interacted_message_id; + + // FIXME: Add this field sometime + /** + * @brief Metadata for the interaction that was used to open the modal, present only on modal submit interactions + */ + // interaction_metadata_type triggering_interaction_metadata; +}; + /** * @brief Message Reference type */ @@ -2214,6 +2251,11 @@ struct DPP_EXPORT message : public managed, json_interface { user usr; } interaction; + /** + * @brief Sent if the message is sent as a result of an interaction + */ + interaction_metadata_type interaction_metadata; + /** * @brief Allowed mentions details */ diff --git a/src/dpp/application.cpp b/src/dpp/application.cpp index 1d2878d96d..eaa0f14f91 100644 --- a/src/dpp/application.cpp +++ b/src/dpp/application.cpp @@ -27,6 +27,21 @@ namespace dpp { using json = nlohmann::json; +void from_json(const json &j, application_integration_types& out) { + out = static_cast(j.get()); +} + +void from_json(const json &j, application_install_params& out) { + out.permissions = j.at("permissions").get(); + j.at("scopes").get_to(out.scopes); +} + +void from_json(const json &j, integration_configuration& out) { + if (auto it = j.find("oauth2_install_params"); it != j.end()) { + it->get_to(out.oauth2_install_params.value()); + } +} + application::application() : managed(0), bot_public(false), bot_require_code_grant(false), guild_id(0), primary_sku_id(0), flags(0) { } @@ -116,6 +131,10 @@ application& application::fill_from_json_impl(nlohmann::json* j) { } } + if (auto it = j->find("integration_types_config"); it != j->end()) { + it->get_to(this->integration_types_config); + } + set_string_not_null(j, "custom_install_url", custom_install_url); // TODO: Investigate https://discord.com/developers/docs/resources/application#application-resource when v11 releases. See if the variables below are documented. diff --git a/src/dpp/message.cpp b/src/dpp/message.cpp index cb96f4edaa..76c14495c4 100644 --- a/src/dpp/message.cpp +++ b/src/dpp/message.cpp @@ -649,14 +649,20 @@ std::optional poll::get_vote_count(uint32_t answer_id) const noexcept return 0; } - +void from_json(const json& j, interaction_metadata_type& i) { + i.id = snowflake_not_null(&j, "id"); + i.interacted_message_id = snowflake_not_null(&j, "interacted_message_id"); + i.original_response_message_id = snowflake_not_null(&j, "original_response_message_id"); + i.type = j["type"]; + i.usr = j["usr"]; +} embed::~embed() = default; embed::embed() : timestamp(0) { } -message::message() : managed(0), channel_id(0), guild_id(0), sent(0), edited(0), webhook_id(0), +message::message() : managed(0), channel_id(0), guild_id(0), sent(0), edited(0), webhook_id(0), interaction_metadata{}, owner(nullptr), type(mt_default), flags(0), pinned(false), tts(false), mention_everyone(false) { message_reference.channel_id = 0; @@ -1330,6 +1336,11 @@ message& message::fill_from_json(json* d, cache_policy_t cp) { this->author = *authoruser; } } + + if (auto it = d->find("interaction_medata"); it != d->end()) { + it->get_to(this->interaction_metadata); + } + if (d->find("interaction") != d->end()) { json& inter = (*d)["interaction"]; interaction.id = snowflake_not_null(&inter, "id"); diff --git a/src/dpp/slashcommand.cpp b/src/dpp/slashcommand.cpp index 782393ccde..9d3b5ed894 100644 --- a/src/dpp/slashcommand.cpp +++ b/src/dpp/slashcommand.cpp @@ -23,7 +23,8 @@ #include #include #include -#include +#include +#include namespace dpp { @@ -73,6 +74,14 @@ slashcommand& slashcommand::fill_from_json_impl(nlohmann::json* j) { type = (slashcommand_contextmenu_type)int8_not_null(j, "type"); set_object_array_not_null(j, "options", options); // command_option fills recursive + + if (auto it = j->find("integration_types"); it != j->end()) { + it->get_to(this->integration_types); + } + + if (auto it = j->find("contexts"); it != j->end()) { + it->get_to(this->contexts); + } return *this; } @@ -251,6 +260,15 @@ void to_json(json& j, const slashcommand& p) { } } + if (p.integration_types.size()) { + j["integration_types"] = p.integration_types; + } + + // TODO: Maybe a std::optional is better to differentiate + if (p.contexts.size()) { + j["contexts"] = p.contexts; + } + // DEPRECATED // j["default_permission"] = p.default_permission; j["application_id"] = std::to_string(p.application_id); @@ -293,6 +311,11 @@ slashcommand& slashcommand::set_application_id(snowflake i) { return *this; } +slashcommand& slashcommand::set_interaction_contexts(std::vector contexts) { + this->contexts = std::move(contexts); + return *this; +} + slashcommand& slashcommand::add_permission(const command_permission& p) { this->permissions.emplace_back(p); return *this; @@ -727,6 +750,18 @@ void from_json(const nlohmann::json& j, interaction& i) { } } + if (auto it = j.find("context"); it != j.end()) { + i.context = static_cast(*it); + } + + if (auto it = j.find("authorizing_integration_owners"); it != j.end()) { + for (auto owner = it->begin(); owner != it->end(); ++owner) { + auto type = static_cast(from_string(owner.key())); + std::string owner_flake = owner.value(); + i.authorizing_integration_owners[type] = dpp::snowflake(owner_flake); + } + } + if(j.contains("entitlements")) { for (auto& entitle : j["entitlements"]) { i.entitlements.emplace_back(entitlement().fill_from_json(const_cast(&entitle))); @@ -734,6 +769,24 @@ void from_json(const nlohmann::json& j, interaction& i) { } } +dpp::snowflake interaction::get_authorizing_integration_owner(application_integration_types type) const { + dpp::snowflake rv; + auto i = this->authorizing_integration_owners.find(type); + if (i != this->authorizing_integration_owners.end()) { + rv = i->second; + } + return rv; +} + +bool interaction::is_user_app_interaction() const { + return this->authorizing_integration_owners.find(ait_user_install) != this->authorizing_integration_owners.end(); +} + +bool interaction::is_guild_interaction() const { + return this->authorizing_integration_owners.find(ait_guild_install) != this->authorizing_integration_owners.end(); +} + + interaction_response& interaction_response::add_autocomplete_choice(const command_option_choice& achoice) { if (autocomplete_choices.size() < AUTOCOMPLETE_MAX_CHOICES) { this->autocomplete_choices.emplace_back(achoice); diff --git a/src/userapptest/userapp.cpp b/src/userapptest/userapp.cpp new file mode 100644 index 0000000000..99f75e68fd --- /dev/null +++ b/src/userapptest/userapp.cpp @@ -0,0 +1,62 @@ +/************************************************************************************ + * + * 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 + +int main() { + char* t = getenv("DPP_UNIT_TEST_TOKEN"); + + if (!t) { + std::cerr << "Missing DPP_UNIT_TEST_TOKEN\n"; + exit(1); + } + + dpp::cluster bot(t, dpp::i_default_intents); + bot.on_log(dpp::utility::cout_logger()); + + bot.on_ready([&bot](const auto& event) { + if (dpp::run_once()) { + /** + * Create a slash command which has interaction context 'itc_private_channel'. + * 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) + .set_interaction_contexts({dpp::itc_guild, dpp::itc_bot_dm, dpp::itc_private_channel}) + }); + } + }); + + 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() + )); + }); + + bot.start(dpp::st_wait); +}