Skip to content

Commit

Permalink
feat: added support for Premium App Subscriptions (#969)
Browse files Browse the repository at this point in the history
  • Loading branch information
Jaskowicz1 authored Oct 25, 2023
1 parent a6cd935 commit 3eb549d
Show file tree
Hide file tree
Showing 21 changed files with 1,170 additions and 31 deletions.
4 changes: 3 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -133,7 +133,9 @@
"cvtps",
"neww",
"STDCORO",
"NOMINMAX"
"NOMINMAX",
"sku",
"skus"
],
"flagWords": [
"hte"
Expand Down
165 changes: 138 additions & 27 deletions include/dpp/appcommand.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
#include <dpp/channel.h>
#include <dpp/role.h>
#include <dpp/user.h>
#include <dpp/entitlement.h>
#include <variant>
#include <map>
#include <dpp/json_fwd.h>
Expand Down Expand Up @@ -297,13 +298,51 @@ void to_json(nlohmann::json& j, const command_option& opt);
* @brief Response types when responding to an interaction within on_interaction_create.
*/
enum interaction_response_type {
ir_pong = 1, //!< Acknowledge a Ping
ir_channel_message_with_source = 4, //!< respond to an interaction with a message
ir_deferred_channel_message_with_source = 5, //!< Acknowledge an interaction and edit a response later, the user sees a loading state
ir_deferred_update_message = 6, //!< for components, acknowledge an interaction and edit the original message later; the user does not see a loading state
ir_update_message = 7, //!< for components, edit the message the component was attached to
ir_autocomplete_reply = 8, //!< Reply to autocomplete interaction. Be sure to do this within 500ms of the interaction!
ir_modal_dialog = 9, //!< A modal dialog box
/**
* @brief Acknowledge a Ping
*/
ir_pong = 1,
/**
* @brief Respond to an interaction with a message.
*/
ir_channel_message_with_source = 4,
/**
* @brief Acknowledge an interaction and edit a response later, the user sees a loading state
*/
ir_deferred_channel_message_with_source = 5,

/**
* @brief For components, acknowledge an interaction and edit the original message later; the user does not see a loading state.
*/
ir_deferred_update_message = 6,

/**
* @brief For components, edit the message the component was attached to.
*/
ir_update_message = 7,

/**
* @brief Reply to autocomplete interaction.
*
* @note Be sure to do this within 500ms of the interaction!
*/
ir_autocomplete_reply = 8,

/**
* @brief A modal dialog box
*
* @note Not available for modal submit and ping interactions.
*/
ir_modal_dialog = 9,

/**
* @brief Acknowledge a interaction with an upgrade button, only available for apps with monetization enabled.
*
* @see https://discord.com/developers/docs/monetization/entitlements#premiumrequired-interaction-response
* @note Not available for autocomplete and ping interactions.
* @warning This response does not support using `content`, `embeds`, or `attachments`, so reply with no data when using this!
*/
ir_premium_required = 10,
};

/**
Expand Down Expand Up @@ -732,26 +771,98 @@ class DPP_EXPORT interaction : public managed, public json_interface<interaction
virtual json to_json_impl(bool with_id = false) const;

public:
snowflake application_id; //!< id of the application this interaction is for
uint8_t type; //!< the type of interaction (dpp::interaction_type)
std::variant<command_interaction, component_interaction, autocomplete_interaction> data; //!< Optional: the command data payload
snowflake guild_id; //!< Optional: the guild it was sent from
snowflake channel_id; //!< Optional: the channel it was sent from
dpp::channel channel; //!< Optional: The partial channel object where it was sent from
snowflake message_id; //!< Originating message id for context menu actions
permission app_permissions; //!< Permissions of the bot in the channel/guild where this command was issued
message msg; //!< Originating message for context menu actions
guild_member member; //!< Optional: guild member data for the invoking user, including permissions. Filled when the interaction is invoked in a guild
user usr; //!< User object for the invoking user
std::string token; //!< a continuation token for responding to the interaction
uint8_t version; //!< read-only property, always 1
command_resolved resolved; //!< Resolved data e.g. users, members, roles, channels, permissions, etc.
std::string locale; //!< User's [locale](https://discord.com/developers/docs/reference#locales) (language)
std::string guild_locale; //!< Guild's locale (language) - for guild interactions only
cache_policy_t cache_policy; //!< Cache policy from cluster

/**
* @brief Construct a new interaction object
/**
* @brief ID of the application this interaction is for.
*/
snowflake application_id;

/**
* @brief The type of interaction from dpp::interaction_type.
*/
uint8_t type;

/**
* @brief Optional: the command data payload.
*/
std::variant<command_interaction, component_interaction, autocomplete_interaction> data;

/**
* @brief Optional: the guild it was sent from.
*/
snowflake guild_id;

/**
* @brief Optional: the channel it was sent from
*/
snowflake channel_id;

/**
* @brief Optional: The partial channel object where it was sent from.
*/
dpp::channel channel;

/**
* @brief Originating message id for context menu actions.
*/
snowflake message_id;

/**
* @brief Permissions of the bot in the channel/guild where this command was issued.
*/
permission app_permissions;

/**
* @brief Originating message for context menu actions.
*/
message msg;

/**
* @brief Optional: guild member data for the invoking user, including permissions. Filled when the interaction is invoked in a guild
*/
guild_member member;

/**
* @brief User object for the invoking user.
*/
user usr;

/**
* @brief A continuation token for responding to the interaction.
*/
std::string token;

/**
* @brief Read-only property, always 1.
*/
uint8_t version;

/**
* @brief Resolved data e.g. users, members, roles, channels, permissions, etc.
*/
command_resolved resolved;

/**
* @brief User's [locale](https://discord.com/developers/docs/reference#locales) (language).
*/
std::string locale;

/**
* @brief Guild's locale (language) - for guild interactions only.
*/
std::string guild_locale;

/**
* @brief Cache policy from cluster.
*/
cache_policy_t cache_policy;

/**
* @brief For monetized apps, any entitlements for the invoking user, representing access to premium SKUs.
*/
std::vector<entitlement> entitlements;

/**
* @brief Construct a new interaction object.
*/
interaction();

Expand Down
81 changes: 81 additions & 0 deletions include/dpp/cluster.h
Original file line number Diff line number Diff line change
Expand Up @@ -1312,6 +1312,36 @@ class DPP_EXPORT cluster {
*/
event_router_t<stage_instance_delete_t> on_stage_instance_delete;

/**
* @brief Called when a user subscribes to an SKU.
*
* @see https://discord.com/developers/docs/monetization/entitlements#new-entitlement
* @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID.
* The function signature for this event takes a single `const` reference of type channel_delete_t&, and returns void.
*/
event_router_t<entitlement_create_t> on_entitlement_create;


/**
* @brief Called when a user's subscription renews for the next billing period.
* The `ends_at` field will have an updated value with the new expiration date.
*
* @see https://discord.com/developers/docs/monetization/entitlements#updated-entitlement
* @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID.
* The function signature for this event takes a single `const` reference of type channel_update_t&, and returns void.
*/
event_router_t<entitlement_update_t> on_entitlement_update;

/**
* @brief Called when a user's entitlement is deleted.
* These events are infrequent and only occur if Discord issues a refund, or Discord removes an entitlement via "internal tooling".
* Entitlements **are not deleted** when they expire.
*
* @see https://discord.com/developers/docs/monetization/entitlements#deleted-entitlement
* @note Use operator() to attach a lambda to this event, and the detach method to detach the listener using the returned ID.
* The function signature for this event takes a single `const` reference of type channel_update_t&, and returns void.
*/
event_router_t<entitlement_delete_t> on_entitlement_delete;

/**
* @brief Post a REST request. Where possible use a helper method instead like message_create
Expand Down Expand Up @@ -3624,6 +3654,57 @@ class DPP_EXPORT cluster {
*/
void automod_rule_delete(snowflake guild_id, snowflake rule_id, command_completion_event_t callback = utility::log_error());

/**
* @brief Returns all entitlements for a given app, active and expired.
*
* @see https://discord.com/developers/docs/monetization/entitlements#list-entitlements
* @param user_id User ID to look up entitlements for.
* @param sku_ids List of SKU IDs to check entitlements for.
* @param before_id Retrieve entitlements before this entitlement ID.
* @param after_id Retrieve entitlements after this entitlement ID.
* @param limit Number of entitlements to return, 1-100 (default 100).
* @param guild_id Guild ID to look up entitlements for.
* @param exclude_ended Whether ended entitlements should be excluded from the search.
* @param callback Function to call when the API call completes.
* On success the callback will contain a dpp::emoji_map object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error().
*/
void entitlements_get(snowflake user_id = 0, const std::vector<snowflake>& sku_ids = {}, snowflake before_id = 0, snowflake after_id = 0, uint8_t limit = 100, snowflake guild_id = 0, bool exclude_ended = false, command_completion_event_t callback = utility::log_error());

/**
* @brief Creates a test entitlement to a given SKU for a given guild or user.
* Discord will act as though that user or guild has entitlement to your premium offering.
*
* @see https://discord.com/developers/docs/monetization/entitlements#create-test-entitlement
* @param new_entitlement The entitlement to create.
* Make sure your dpp::entitlement_type (inside your dpp::entitlement object) matches the type of the owner_id
* (if type is guild, owner_id is a guild id), otherwise it won't work!
* @param callback Function to call when the API call completes.
* On success the callback will contain a dpp::entitlement object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error().
*/
void entitlement_test_create(const class entitlement& new_entitlement, command_completion_event_t callback = utility::log_error());

/**
* @brief Deletes a currently-active test entitlement.
* Discord will act as though that user or guild no longer has entitlement to your premium offering.
*
* @see https://discord.com/developers/docs/monetization/entitlements#delete-test-entitlement
* @param entitlement_id The test entitlement to delete.
* @param callback Function to call when the API call completes.
* On success the callback will contain a dpp::confirmation object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error().
*/
void entitlement_test_delete(snowflake entitlement_id, command_completion_event_t callback = utility::log_error());

/**
* @brief Returns all SKUs for a given application.
* @note Because of how Discord's SKU and subscription systems work, you will see two SKUs for your premium offering.
* For integration and testing entitlements, you should use the SKU with type: 5.
*
* @see https://discord.com/developers/docs/monetization/skus#list-skus
* @param callback Function to call when the API call completes.
* On success the callback will contain a dpp::confirmation object in confirmation_callback_t::value. On failure, the value is undefined and confirmation_callback_t::is_error() method will return true. You can obtain full error details with confirmation_callback_t::get_error().
*/
void skus_get(command_completion_event_t callback = utility::log_error());

#include <dpp/cluster_sync_calls.h>
#ifdef DPP_CORO
#include <dpp/cluster_coro_calls.h>
Expand Down
55 changes: 55 additions & 0 deletions include/dpp/cluster_coro_calls.h
Original file line number Diff line number Diff line change
Expand Up @@ -696,6 +696,49 @@
*/
[[nodiscard]] async<confirmation_callback_t> co_guild_emojis_get(snowflake guild_id);

/**
* @brief Returns all entitlements for a given app, active and expired.
*
* @see dpp::cluster::entitlements_get
* @see https://discord.com/developers/docs/monetization/entitlements#list-entitlements
* @param user_id User ID to look up entitlements for.
* @param sku_ids List of SKU IDs to check entitlements for.
* @param before_id Retrieve entitlements before this entitlement ID.
* @param after_id Retrieve entitlements after this entitlement ID.
* @param limit Number of entitlements to return, 1-100 (default 100).
* @param guild_id Guild ID to look up entitlements for.
* @param exclude_ended Whether ended entitlements should be excluded from the search.
* @return entitlement_map returned object on completion
* \memberof dpp::cluster
*/
[[nodiscard]] async<confirmation_callback_t> co_entitlements_get(snowflake user_id = 0, const std::vector<snowflake>& sku_ids = {}, snowflake before_id = 0, snowflake after_id = 0, uint8_t limit = 100, snowflake guild_id = 0, bool exclude_ended = false);

/**
* @brief Creates a test entitlement to a given SKU for a given guild or user.
* Discord will act as though that user or guild has entitlement to your premium offering.
*
* @see dpp::cluster::entitlement_test_create
* @see https://discord.com/developers/docs/monetization/entitlements#create-test-entitlement
* @param new_entitlement The entitlement to create.
* Make sure your dpp::entitlement_type (inside your dpp::entitlement object) matches the type of the owner_id
* (if type is guild, owner_id is a guild id), otherwise it won't work!
* @return entitlement returned object on completion
* \memberof dpp::cluster
*/
[[nodiscard]] async<confirmation_callback_t> co_entitlement_test_create(const class entitlement& new_entitlement);

/**
* @brief Deletes a currently-active test entitlement.
* Discord will act as though that user or guild no longer has entitlement to your premium offering.
*
* @see dpp::cluster::entitlement_test_delete
* @see https://discord.com/developers/docs/monetization/entitlements#delete-test-entitlement
* @param entitlement_id The test entitlement to delete.
* @return confirmation returned object on completion
* \memberof dpp::cluster
*/
[[nodiscard]] async<confirmation_callback_t> co_entitlement_test_delete(snowflake entitlement_id);

/**
* @brief Get the gateway information for the bot using the token
* @see dpp::cluster::get_gateway_bot
Expand Down Expand Up @@ -1727,6 +1770,18 @@
*/
[[nodiscard]] async<confirmation_callback_t> co_guild_event_get(snowflake guild_id, snowflake event_id);

/**
* @brief Returns all SKUs for a given application.
* @note Because of how Discord's SKU and subscription systems work, you will see two SKUs for your premium offering.
* For integration and testing entitlements, you should use the SKU with type: 5.
*
* @see dpp::cluster::skus_get
* @see https://discord.com/developers/docs/monetization/skus#list-skus
* @return sku_map returned object on completion
* \memberof dpp::cluster
*/
[[nodiscard]] async<confirmation_callback_t> co_skus_get();


[[nodiscard]] async<confirmation_callback_t> co_stage_instance_create(const stage_instance& si);

Expand Down
Loading

0 comments on commit 3eb549d

Please sign in to comment.