Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Entitlements / SKUs #1552

Open
wants to merge 29 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
f004123
add entitlement struct
jhoffi Aug 2, 2024
2e7eb30
add entitlements field to interaction struct
jhoffi Aug 2, 2024
4bbe559
add event handler for entitlements
jhoffi Aug 2, 2024
84ecf01
add description to Entitlement struct & type
jhoffi Aug 2, 2024
6725cb4
properly format changes in entitlement and events
jhoffi Aug 2, 2024
ccb5a90
format entitlement with gofmt
jhoffi Aug 2, 2024
7ad7e1b
move entitlement to structs
jhoffi Aug 2, 2024
87c3490
add subscription id to entitlement struct and add omitempty
jhoffi Aug 2, 2024
9620435
add endpoints
jhoffi Aug 2, 2024
38a1aff
add comments to EntitlementTest and EntitlementOwnerType
jhoffi Aug 2, 2024
efb7d7e
add sku struct
jhoffi Aug 2, 2024
351f5e9
add skus
jhoffi Aug 2, 2024
7bf00e8
fix typo
jhoffi Aug 2, 2024
d2057dc
rename skuid in entitlement struct
jhoffi Aug 2, 2024
b7ed8b8
add comments to SKUFlags
jhoffi Aug 2, 2024
5b3da13
change entitlement timestamps to time.Time
jhoffi Aug 2, 2024
34c29b0
rename skuid in entitlement-test struct
jhoffi Aug 3, 2024
6eb85e1
add documentation
jhoffi Aug 3, 2024
faf1279
remove undocumented subscriptionID field in entitlement struct
jhoffi Aug 3, 2024
887cdb3
add documentation
jhoffi Aug 3, 2024
20d5f76
update documentation
jhoffi Aug 14, 2024
54287e3
change order of EntitlementType values
jhoffi Aug 14, 2024
7cf6c73
move optional entitlement filter options into a separate struct
jhoffi Aug 15, 2024
2c97651
add premium button component
jhoffi Oct 5, 2024
e9b116e
add subscriptions
jhoffi Oct 5, 2024
d9d9f4b
optimize optional fields in entitlement struct
jhoffi Oct 5, 2024
621e4ad
Add documentation
jhoffi Oct 5, 2024
f81b609
typo
jhoffi Oct 5, 2024
347e932
remove unneccessary pointers
jhoffi Oct 15, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions components.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,8 @@ const (
DangerButton ButtonStyle = 4
// LinkButton is a special type of button which navigates to a URL. Has grey color.
LinkButton ButtonStyle = 5
// PremiumButton is a special type of button with a blurple color that links to a SKU.
PremiumButton ButtonStyle = 6
)

// ComponentEmoji represents button emoji, if it does have one.
Expand All @@ -140,6 +142,8 @@ type Button struct {
// NOTE: Only button with LinkButton style can have link. Also, URL is mutually exclusive with CustomID.
URL string `json:"url,omitempty"`
CustomID string `json:"custom_id,omitempty"`
// Identifier for a purchasable SKU. Only available when using premium-style buttons.
SKUID string `json:"sku_id,omitempty"`
}

// MarshalJSON is a method for marshaling Button to a JSON object.
Expand Down
10 changes: 9 additions & 1 deletion endpoints.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ var (
EndpointWebhooks = EndpointAPI + "webhooks/"
EndpointStickers = EndpointAPI + "stickers/"
EndpointStageInstances = EndpointAPI + "stage-instances"
EndpointSKUs = EndpointAPI + "skus"

EndpointCDN = "https://cdn.discordapp.com/"
EndpointCDNAttachments = EndpointCDN + "attachments/"
Expand Down Expand Up @@ -172,7 +173,7 @@ var (
return EndpointPoll(cID, mID) + "/expire"
}

EndpointSKUs = func(aID string) string {
EndpointApplicationSKUs = func(aID string) string {
return EndpointApplication(aID) + "/skus"
}

Expand All @@ -186,6 +187,13 @@ var (
return EndpointEntitlement(aID, eID) + "/consume"
}

EndpointSubscriptions = func(skuID string) string {
return EndpointSKUs + "/" + skuID + "/subscriptions"
}
EndpointSubscription = func(skuID, subID string) string {
return EndpointSubscriptions(skuID) + "/" + subID
}

EndpointApplicationGlobalCommands = func(aID string) string {
return EndpointApplication(aID) + "/commands"
}
Expand Down
72 changes: 61 additions & 11 deletions restapi.go
Original file line number Diff line number Diff line change
Expand Up @@ -3502,13 +3502,13 @@ func (s *Session) PollExpire(channelID, messageID string) (msg *Message, err err
}

// ----------------------------------------------------------------------
// Functions specific to SKUs
// Functions specific to monetization
// ----------------------------------------------------------------------

// SKUs returns all SKUs for a given application.
// appID : The ID of the application.
func (s *Session) SKUs(appID string) (skus []*SKU, err error) {
endpoint := EndpointSKUs(appID)
endpoint := EndpointApplicationSKUs(appID)

body, err := s.RequestWithBucketID("GET", endpoint, nil, endpoint)
if err != nil {
Expand All @@ -3519,11 +3519,7 @@ func (s *Session) SKUs(appID string) (skus []*SKU, err error) {
return
}

// ----------------------------------------------------------------------
// Functions specific to entitlements
// ----------------------------------------------------------------------

// Entitlements returns all antitlements for a given app, active and expired.
// Entitlements returns all Entitlements for a given app, active and expired.
// appID : The ID of the application.
// filterOptions : Optional filter options; otherwise set it to nil.
func (s *Session) Entitlements(appID string, filterOptions *EntitlementFilterOptions, options ...RequestOption) (entitlements []*Entitlement, err error) {
Expand All @@ -3537,11 +3533,11 @@ func (s *Session) Entitlements(appID string, filterOptions *EntitlementFilterOpt
if filterOptions.SkuIDs != nil && len(filterOptions.SkuIDs) > 0 {
queryParams.Set("sku_ids", strings.Join(filterOptions.SkuIDs, ","))
}
if filterOptions.BeforeID != "" {
queryParams.Set("before", filterOptions.BeforeID)
if filterOptions.Before != nil {
queryParams.Set("before", filterOptions.Before.Format(time.RFC3339))
}
if filterOptions.AfterID != "" {
queryParams.Set("after", filterOptions.AfterID)
if filterOptions.After != nil {
queryParams.Set("after", filterOptions.After.Format(time.RFC3339))
}
if filterOptions.Limit > 0 {
queryParams.Set("limit", strconv.Itoa(filterOptions.Limit))
Expand Down Expand Up @@ -3584,3 +3580,57 @@ func (s *Session) EntitlementTestDelete(appID, entitlementID string, options ...
_, err = s.RequestWithBucketID("DELETE", EndpointEntitlement(appID, entitlementID), nil, EndpointEntitlement(appID, ""), options...)
return
}

// Subscriptions returns all subscriptions containing the SKU.
// skuID : The ID of the SKU.
// userID : User ID for which to return subscriptions. Required except for OAuth queries.
// before : Optional timestamp to retrieve subscriptions before this time.
// after : Optional timestamp to retrieve subscriptions after this time.
// limit : Optional maximum number of subscriptions to return (1-100, default 50).
func (s *Session) Subscriptions(skuID string, userID string, before, after *time.Time, limit int, options ...RequestOption) (subscriptions []*Subscription, err error) {
endpoint := EndpointSubscriptions(skuID)

queryParams := url.Values{}
if before != nil {
queryParams.Set("before", before.Format(time.RFC3339))
}
if after != nil {
queryParams.Set("after", after.Format(time.RFC3339))
}
if userID != "" {
queryParams.Set("user_id", userID)
}
if limit > 0 {
queryParams.Set("limit", strconv.Itoa(limit))
}

body, err := s.RequestWithBucketID("GET", endpoint+"?"+queryParams.Encode(), nil, endpoint, options...)
if err != nil {
return
}

err = unmarshal(body, &subscriptions)
return
}

// Subscription returns a subscription by its SKU and subscription ID.
// skuID : The ID of the SKU.
// subscriptionID : The ID of the subscription.
// userID : User ID for which to return the subscription. Required except for OAuth queries.
func (s *Session) Subscription(skuID, subscriptionID, userID string, options ...RequestOption) (subscription *Subscription, err error) {
endpoint := EndpointSubscription(skuID, subscriptionID)

queryParams := url.Values{}
if userID != "" {
// Unlike stated in the documentation, the user_id parameter is required here.
queryParams.Set("user_id", userID)
}

body, err := s.RequestWithBucketID("GET", endpoint+"?"+queryParams.Encode(), nil, endpoint, options...)
if err != nil {
return
}

err = unmarshal(body, &subscription)
return
}
62 changes: 54 additions & 8 deletions structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -2437,6 +2437,48 @@ type SKU struct {
Flags SKUFlags `json:"flags"`
}

// Subscription represents a user making recurring payments for at least one SKU over an ongoing period.
// https://discord.com/developers/docs/resources/subscription#subscription-object
type Subscription struct {
// ID of the subscription
ID string `json:"id"`

// ID of the user who is subscribed
UserID string `json:"user_id"`

// List of SKUs subscribed to
SKUIDs []string `json:"sku_ids"`

// List of entitlements granted for this subscription
EntitlementIDs []string `json:"entitlement_ids"`

// Start of the current subscription period
CurrentPeriodStart time.Time `json:"current_period_start"`

// End of the current subscription period
CurrentPeriodEnd time.Time `json:"current_period_end"`

// Current status of the subscription
Status SubscriptionStatus `json:"status"`

// When the subscription was canceled. Only present if the subscription has been canceled.
CanceledAt *time.Time `json:"canceled_at,omitempty"`

// ISO3166-1 alpha-2 country code of the payment source used to purchase the subscription. Missing unless queried with a private OAuth scope.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1 for including country code type

Country *string `json:"country,omitempty"`
}

// SubscriptionStatus is the current status of a Subscription Object
// https://discord.com/developers/docs/resources/subscription#subscription-statuses
type SubscriptionStatus int

// Valid SubscriptionStatus values
const (
SubscriptionStatusActive = 0
SubscriptionStatusEnding = 1
SubscriptionStatusInactive = 2
)

// EntitlementType is the type of entitlement (see EntitlementType* consts)
// https://discord.com/developers/docs/monetization/entitlements#entitlement-object-entitlement-types
type EntitlementType int
Expand Down Expand Up @@ -2467,7 +2509,7 @@ type Entitlement struct {

// The ID of the user that is granted access to the entitlement's sku
// Only available for user subscriptions.
UserID string `json:"user_id,omitempty"`
UserID *string `json:"user_id,omitempty"`

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there ever a point at which we'd expect/want a default string value here? Wondering if the introduction of pointer risk is necessary or if we can roll with default values without losing any important data (no user has no ID, no guild has no ID, no entitlement has a start time at zero, etc.)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are right, thank you. However, I would keep all the optional timestamps as pointers, since this is also done in other structs (e.g., Channel, Member, Invite).


// The type of the entitlement
Type EntitlementType `json:"type"`
Expand All @@ -2480,16 +2522,20 @@ type Entitlement struct {
StartsAt *time.Time `json:"starts_at,omitempty"`

// The date at which the entitlement is no longer valid.
// Not present when using test entitlements.
// Not present when using test entitlements or when receiving an ENTITLEMENT_CREATE event.
EndsAt *time.Time `json:"ends_at,omitempty"`

// The ID of the guild that is granted access to the entitlement's sku.
// Only available for guild subscriptions.
GuildID string `json:"guild_id,omitempty"`
GuildID *string `json:"guild_id,omitempty"`

// Whether or not the entitlement has been consumed.
// Only available for consumable items.
Consumed bool `json:"consumed,omitempty"`
Consumed *bool `json:"consumed,omitempty"`

// The SubscriptionID of the entitlement.
// Not present when using test entitlements.
SubscriptionID *string `json:"subscription_id,omitempty"`
}

// EntitlementOwnerType is the type of entitlement (see EntitlementOwnerType* consts)
Expand Down Expand Up @@ -2521,11 +2567,11 @@ type EntitlementFilterOptions struct {
// Optional array of SKU IDs to check for.
SkuIDs []string

// Optional timestamp (snowflake) to retrieve Entitlements before this time.
BeforeID string
// Optional timestamp to retrieve Entitlements before this time.
Before *time.Time

// Optional timestamp (snowflake) to retrieve Entitlements after this time.
AfterID string
// Optional timestamp to retrieve Entitlements after this time.
After *time.Time

// Optional maximum number of entitlements to return (1-100, default 100).
Limit int
Expand Down
Loading