diff --git a/components.go b/components.go index af3d7aba5..7d320780e 100644 --- a/components.go +++ b/components.go @@ -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. @@ -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. diff --git a/endpoints.go b/endpoints.go index 29189af69..3e1a5bd03 100644 --- a/endpoints.go +++ b/endpoints.go @@ -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/" @@ -172,6 +173,27 @@ var ( return EndpointPoll(cID, mID) + "/expire" } + EndpointApplicationSKUs = func(aID string) string { + return EndpointApplication(aID) + "/skus" + } + + EndpointEntitlements = func(aID string) string { + return EndpointApplication(aID) + "/entitlements" + } + EndpointEntitlement = func(aID, eID string) string { + return EndpointEntitlements(aID) + "/" + eID + } + EndpointEntitlementConsume = func(aID, eID string) string { + 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" } diff --git a/eventhandlers.go b/eventhandlers.go index 659b6748e..06df984bd 100644 --- a/eventhandlers.go +++ b/eventhandlers.go @@ -18,6 +18,9 @@ const ( channelUpdateEventType = "CHANNEL_UPDATE" connectEventType = "__CONNECT__" disconnectEventType = "__DISCONNECT__" + entitlementCreateEventType = "ENTITLEMENT_CREATE" + entitlementDeleteEventType = "ENTITLEMENT_DELETE" + entitlementUpdateEventType = "ENTITLEMENT_UPDATE" eventEventType = "__EVENT__" guildAuditLogEntryCreateEventType = "GUILD_AUDIT_LOG_ENTRY_CREATE" guildBanAddEventType = "GUILD_BAN_ADD" @@ -285,6 +288,66 @@ func (eh disconnectEventHandler) Handle(s *Session, i interface{}) { } } +// entitlementCreateEventHandler is an event handler for EntitlementCreate events. +type entitlementCreateEventHandler func(*Session, *EntitlementCreate) + +// Type returns the event type for EntitlementCreate events. +func (eh entitlementCreateEventHandler) Type() string { + return entitlementCreateEventType +} + +// New returns a new instance of EntitlementCreate. +func (eh entitlementCreateEventHandler) New() interface{} { + return &EntitlementCreate{} +} + +// Handle is the handler for EntitlementCreate events. +func (eh entitlementCreateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*EntitlementCreate); ok { + eh(s, t) + } +} + +// entitlementDeleteEventHandler is an event handler for EntitlementDelete events. +type entitlementDeleteEventHandler func(*Session, *EntitlementDelete) + +// Type returns the event type for EntitlementDelete events. +func (eh entitlementDeleteEventHandler) Type() string { + return entitlementDeleteEventType +} + +// New returns a new instance of EntitlementDelete. +func (eh entitlementDeleteEventHandler) New() interface{} { + return &EntitlementDelete{} +} + +// Handle is the handler for EntitlementDelete events. +func (eh entitlementDeleteEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*EntitlementDelete); ok { + eh(s, t) + } +} + +// entitlementUpdateEventHandler is an event handler for EntitlementUpdate events. +type entitlementUpdateEventHandler func(*Session, *EntitlementUpdate) + +// Type returns the event type for EntitlementUpdate events. +func (eh entitlementUpdateEventHandler) Type() string { + return entitlementUpdateEventType +} + +// New returns a new instance of EntitlementUpdate. +func (eh entitlementUpdateEventHandler) New() interface{} { + return &EntitlementUpdate{} +} + +// Handle is the handler for EntitlementUpdate events. +func (eh entitlementUpdateEventHandler) Handle(s *Session, i interface{}) { + if t, ok := i.(*EntitlementUpdate); ok { + eh(s, t) + } +} + // eventEventHandler is an event handler for Event events. type eventEventHandler func(*Session, *Event) @@ -1401,6 +1464,12 @@ func handlerForInterface(handler interface{}) EventHandler { return connectEventHandler(v) case func(*Session, *Disconnect): return disconnectEventHandler(v) + case func(*Session, *EntitlementCreate): + return entitlementCreateEventHandler(v) + case func(*Session, *EntitlementDelete): + return entitlementDeleteEventHandler(v) + case func(*Session, *EntitlementUpdate): + return entitlementUpdateEventHandler(v) case func(*Session, *Event): return eventEventHandler(v) case func(*Session, *GuildAuditLogEntryCreate): @@ -1526,6 +1595,9 @@ func init() { registerInterfaceProvider(channelDeleteEventHandler(nil)) registerInterfaceProvider(channelPinsUpdateEventHandler(nil)) registerInterfaceProvider(channelUpdateEventHandler(nil)) + registerInterfaceProvider(entitlementCreateEventHandler(nil)) + registerInterfaceProvider(entitlementDeleteEventHandler(nil)) + registerInterfaceProvider(entitlementUpdateEventHandler(nil)) registerInterfaceProvider(guildAuditLogEntryCreateEventHandler(nil)) registerInterfaceProvider(guildBanAddEventHandler(nil)) registerInterfaceProvider(guildBanRemoveEventHandler(nil)) diff --git a/events.go b/events.go index 6a23a9e23..6a410fe35 100644 --- a/events.go +++ b/events.go @@ -445,3 +445,19 @@ type MessagePollVoteRemove struct { GuildID string `json:"guild_id,omitempty"` AnswerID int `json:"answer_id"` } + +// EntitlementCreate is the data for an EntitlementCreate event. +type EntitlementCreate struct { + *Entitlement +} + +// EntitlementUpdate is the data for an EntitlementUpdate event. +type EntitlementUpdate struct { + *Entitlement +} + +// EntitlementDelete is the data for an EntitlementDelete event. +// NOTE: Entitlements are not deleted when they expire. +type EntitlementDelete struct { + *Entitlement +} diff --git a/interactions.go b/interactions.go index 93ba366bb..774b17ff1 100644 --- a/interactions.go +++ b/interactions.go @@ -255,6 +255,10 @@ type Interaction struct { Token string `json:"token"` Version int `json:"version"` + + // Any entitlements for the invoking user, representing access to premium SKUs. + // NOTE: this field is only filled in monetized apps + Entitlements []*Entitlement `json:"entitlements"` } type interaction Interaction diff --git a/restapi.go b/restapi.go index ffb8e3022..cc3afc724 100644 --- a/restapi.go +++ b/restapi.go @@ -3500,3 +3500,137 @@ func (s *Session) PollExpire(channelID, messageID string) (msg *Message, err err err = unmarshal(body, &msg) return } + +// ---------------------------------------------------------------------- +// 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 := EndpointApplicationSKUs(appID) + + body, err := s.RequestWithBucketID("GET", endpoint, nil, endpoint) + if err != nil { + return + } + + err = unmarshal(body, &skus) + return +} + +// 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) { + endpoint := EndpointEntitlements(appID) + + queryParams := url.Values{} + if filterOptions != nil { + if filterOptions.UserID != "" { + queryParams.Set("user_id", filterOptions.UserID) + } + if filterOptions.SkuIDs != nil && len(filterOptions.SkuIDs) > 0 { + queryParams.Set("sku_ids", strings.Join(filterOptions.SkuIDs, ",")) + } + if filterOptions.Before != nil { + queryParams.Set("before", filterOptions.Before.Format(time.RFC3339)) + } + if filterOptions.After != nil { + queryParams.Set("after", filterOptions.After.Format(time.RFC3339)) + } + if filterOptions.Limit > 0 { + queryParams.Set("limit", strconv.Itoa(filterOptions.Limit)) + } + if filterOptions.GuildID != "" { + queryParams.Set("guild_id", filterOptions.GuildID) + } + if filterOptions.ExcludeEnded { + queryParams.Set("exclude_ended", "true") + } + } + + body, err := s.RequestWithBucketID("GET", endpoint+"?"+queryParams.Encode(), nil, endpoint, options...) + if err != nil { + return + } + + err = unmarshal(body, &entitlements) + return +} + +// EntitlementConsume marks a given One-Time Purchase for the user as consumed. +func (s *Session) EntitlementConsume(appID, entitlementID string, options ...RequestOption) (err error) { + _, err = s.RequestWithBucketID("POST", EndpointEntitlementConsume(appID, entitlementID), nil, EndpointEntitlementConsume(appID, ""), options...) + return +} + +// EntitlementTestCreate 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. +func (s *Session) EntitlementTestCreate(appID string, data *EntitlementTest, options ...RequestOption) (err error) { + endpoint := EndpointEntitlements(appID) + + _, err = s.RequestWithBucketID("POST", endpoint, data, endpoint, options...) + return +} + +// EntitlementTestDelete deletes a currently-active test entitlement. Discord will act as though +// that user or guild no longer has entitlement to your premium offering. +func (s *Session) EntitlementTestDelete(appID, entitlementID string, options ...RequestOption) (err error) { + _, 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 +} diff --git a/structs.go b/structs.go index 475107c0d..7555a70e2 100644 --- a/structs.go +++ b/structs.go @@ -2389,6 +2389,200 @@ type Poll struct { Expiry *time.Time `json:"expiry,omitempty"` } +// SKUType is the type of SKU (see SKUType* consts) +// https://discord.com/developers/docs/monetization/skus +type SKUType int + +// Valid SKUType values +const ( + SKUTypeDurable SKUType = 2 + SKUTypeConsumable SKUType = 3 + SKUTypeSubscription SKUType = 5 + // SKUTypeSubscriptionGroup is a system-generated group for each subscription SKU. + SKUTypeSubscriptionGroup SKUType = 6 +) + +// SKUFlags is a bitfield of flags used to differentiate user and server subscriptions (see SKUFlag* consts) +// https://discord.com/developers/docs/monetization/skus#sku-object-sku-flags +type SKUFlags int + +const ( + // SKUFlagAvailable indicates that the SKU is available for purchase. + SKUFlagAvailable SKUFlags = 1 << 2 + // SKUFlagGuildSubscription indicates that the SKU is a guild subscription. + SKUFlagGuildSubscription SKUFlags = 1 << 7 + // SKUFlagUserSubscription indicates that the SKU is a user subscription. + SKUFlagUserSubscription SKUFlags = 1 << 8 +) + +// SKU (stock-keeping units) represent premium offerings +type SKU struct { + // The ID of the SKU + ID string `json:"id"` + + // The Type of the SKU + Type SKUType `json:"type"` + + // The ID of the parent application + ApplicationID string `json:"application_id"` + + // Customer-facing name of the SKU. + Name string `json:"name"` + + // System-generated URL slug based on the SKU's name. + Slug string `json:"slug"` + + // SKUFlags combined as a bitfield. The presence of a certain flag can be checked + // by performing a bitwise AND operation between this int and the flag. + 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. + 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 + +// Valid EntitlementType values +const ( + EntitlementTypePurchase = 1 + EntitlementTypePremiumSubscription = 2 + EntitlementTypeDeveloperGift = 3 + EntitlementTypeTestModePurchase = 4 + EntitlementTypeFreePurchase = 5 + EntitlementTypeUserGift = 6 + EntitlementTypePremiumPurchase = 7 + EntitlementTypeApplicationSubscription = 8 +) + +// Entitlement represents that a user or guild has access to a premium offering +// in your application. +type Entitlement struct { + // The ID of the entitlement + ID string `json:"id"` + + // The ID of the SKU + SKUID string `json:"sku_id"` + + // The ID of the parent application + ApplicationID string `json:"application_id"` + + // 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"` + + // The type of the entitlement + Type EntitlementType `json:"type"` + + // The entitlement was deleted + Deleted bool `json:"deleted"` + + // The start date at which the entitlement is valid. + // Not present when using test entitlements. + StartsAt *time.Time `json:"starts_at,omitempty"` + + // The date at which the entitlement is no longer valid. + // 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"` + + // Whether or not the entitlement has been consumed. + // Only available for consumable items. + 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) +type EntitlementOwnerType int + +// Valid EntitlementOwnerType values +const ( + EntitlementOwnerTypeGuildSubscription EntitlementOwnerType = 1 + EntitlementOwnerTypeUserSubscription EntitlementOwnerType = 2 +) + +// EntitlementTest is used to test granting an entitlement to a user or guild +type EntitlementTest struct { + // The ID of the SKU to grant the entitlement to + SKUID string `json:"sku_id"` + + // The ID of the guild or user to grant the entitlement to + OwnerID string `json:"owner_id"` + + // OwnerType is the type of which the entitlement should be created + OwnerType EntitlementOwnerType `json:"owner_type"` +} + +// EntitlementFilterOptions are the options for filtering Entitlements +type EntitlementFilterOptions struct { + // Optional user ID to look up for. + UserID string + + // Optional array of SKU IDs to check for. + SkuIDs []string + + // Optional timestamp to retrieve Entitlements before this time. + Before *time.Time + + // Optional timestamp to retrieve Entitlements after this time. + After *time.Time + + // Optional maximum number of entitlements to return (1-100, default 100). + Limit int + + // Optional guild ID to look up for. + GuildID string + + // Optional whether or not ended entitlements should be omitted. + ExcludeEnded bool +} + // Constants for the different bit offsets of text channel permissions const ( // Deprecated: PermissionReadMessages has been replaced with PermissionViewChannel for text and voice channels