diff --git a/esm.mjs b/esm.mjs index 95734bca5..f236a63b7 100644 --- a/esm.mjs +++ b/esm.mjs @@ -22,6 +22,7 @@ export const { DiscordHTTPError, DiscordRESTError, DMChannel, + Entitlement, ExtendedUser, ForumChannel, GroupChannel, @@ -47,6 +48,8 @@ export const { PublicThreadChannel, RequestHandler, Role, + SKU, + Subscription, SequentialBucket, Shard, SharedStream, diff --git a/index.d.ts b/index.d.ts index 6cd2a5d39..bdf151a34 100644 --- a/index.d.ts +++ b/index.d.ts @@ -153,7 +153,7 @@ declare namespace Eris { // Message type ActionRowComponents = Button | SelectMenu; - type Button = InteractionButton | URLButton; + type Button = InteractionButton | URLButton | PremiumButton; type ButtonStyles = Constants["ButtonStyles"][keyof Constants["ButtonStyles"]]; type Component = ActionRow | ActionRowComponents; type ImageFormat = Constants["ImageFormats"][number]; @@ -202,6 +202,13 @@ declare namespace Eris { type WebhookPayloadEdit = Pick; type WebhookTypes = Constants["WebhookTypes"][keyof Constants["WebhookTypes"]]; + // Subscriptions + type EntitlementTypes = Constants["EntitlementTypes"][keyof Constants["EntitlementTypes"]]; + type EntitlementOwnerTypes = Constants["EntitlementOwnerTypes"][keyof Constants["EntitlementOwnerTypes"]]; + type SKUTypes = Constants["SKUTypes"][keyof Constants["SKUTypes"]]; + type SKUFlags = Constants["SKUFlags"][keyof Constants["SKUFlags"]]; + type SubscriptionStatuses = Constants["SubscriptionStatuses"][keyof Constants["SubscriptionStatuses"]]; + // INTERFACES // Internals type JSONCache = Record; @@ -983,6 +990,12 @@ declare namespace Eris { voiceStateUpdate: [member: Member, oldState: OldVoiceState]; warn: [message: string, id?: number]; webhooksUpdate: [data: WebhookData]; + entitlementCreate: [entitlement: Entitlement]; + entitlementUpdate: [entitlement: Entitlement]; + entitlementDelete: [entitlement: Entitlement]; + subscriptionCreate: [subscription: Subscription]; + subscriptionDelete: [subscription: Subscription]; + subscriptionUpdate: [subscription: Subscription]; } interface ClientEvents extends EventListeners { shardDisconnect: [err: Error | undefined, id: number]; @@ -1358,6 +1371,30 @@ declare namespace Eris { reason?: string; } + // Subscriptions + interface CreateTestEntitlementOptions { + skuID: string; + ownerID: string; + ownerType: EntitlementOwnerTypes; + } + + interface GetEntitlementsOptions { + userID?: string; + skuIDs?: string[]; + before?: number; + after?: number; + limit?: number; + guildID?: string; + excludeEnded?: boolean; + } + + interface GetSKUSubscriptionsOptions { + userID: string; + before?: number; + after?: number; + limit?: number; + } + // Interaction interface AutocompleteInteractionData { id: string; @@ -1590,7 +1627,7 @@ declare namespace Eris { } interface InteractionButton extends ButtonBase { custom_id: string; - style: Exclude; + style: Exclude; } interface MessageActivity { party_id?: string; @@ -1741,6 +1778,10 @@ declare namespace Eris { style: Constants["ButtonStyles"]["LINK"]; url: string; } + interface PremiumButton extends ButtonBase { + style: Constants["ButtonStyles"]["PREMIUM"]; + sku_id: string; + } // Presence interface Activity extends ActivityPartial { @@ -2179,6 +2220,7 @@ declare namespace Eris { bulkEditGuildCommands(guildID: string, commands: ApplicationCommandBulkEditOptions[]): Promise[]>; closeVoiceConnection(guildID: string): void; connect(): Promise; + consumeEntitlement(entitlementID: string): Promise; createAutoModerationRule(guildID: string, rule: AutoModerationCreateOptions): Promise; createChannel(guildID: string, name: string): Promise; createChannel(guildID: string, name: string, type: T, options?: CreateChannelOptions): Promise>; @@ -2209,12 +2251,15 @@ declare namespace Eris { createMessage(channelID: string, content: MessageContent, file?: FileContent | FileContent[]): Promise; createRole(guildID: string, options?: Role | RoleOptions, reason?: string): Promise; createStageInstance(channelID: string, options: StageInstanceOptions): Promise; + createTestEntitlement(options: CreateTestEntitlementOptions): Promise; createThread(channelID: string, options: CreateForumThreadOptions, file?: FileContent | FileContent[]): Promise>; createThread(channelID: string, options: CreateThreadWithoutMessageOptions, file?: FileContent | FileContent[]): Promise; createThreadWithMessage(channelID: string, messageID: string, options: CreateThreadOptions): Promise; /** @deprecated */ createThreadWithoutMessage(channelID: string, options: CreateThreadWithoutMessageOptions): Promise; + crosspostMessage(channelID: string, messageID: string): Promise; + deleteAutoModerationRule(guildID: string, ruleID: string, reason?: string): Promise; deleteChannel(channelID: string, reason?: string): Promise; deleteChannelPermission(channelID: string, overwriteID: string, reason?: string): Promise; @@ -2234,6 +2279,7 @@ declare namespace Eris { deleteMessages(channelID: string, messageIDs: string[], reason?: string): Promise; deleteRole(guildID: string, roleID: string, reason?: string): Promise; deleteStageInstance(channelID: string): Promise; + deleteTestEntitlement(entitlementID: string): Promise; deleteWebhook(webhookID: string, token?: string, reason?: string): Promise; deleteWebhookMessage(webhookID: string, token: string, messageID: string): Promise; disconnect(options: { reconnect?: boolean | "auto" }): void; @@ -2324,6 +2370,7 @@ declare namespace Eris { getEmoji(emojiID: string): Promise; getEmojis(): Promise; getEmojiGuild(emojiID: string): Promise; + getEntitlements(options?: GetEntitlementsOptions): Promise; getGateway(): Promise<{ url: string }>; getGuildAuditLog(guildID: string, options?: GetGuildAuditLogOptions): Promise; /** @deprecated */ @@ -2389,6 +2436,11 @@ declare namespace Eris { getRESTUser(userID: string): Promise; getRoleConnectionMetadataRecords(): Promise; getSelf(): Promise; + getSKUs(): Promise; + getSKUSubscription(skuID: string, subscriptionID: string): Promise; + + getSKUSubscriptions(skuID: string, options: GetSKUSubscriptionsOptions): Promise; + getSoundboardSounds(): Promise[]>; getStageInstance(channelID: string): Promise; getStickerPack(packID: string): Promise; @@ -2567,6 +2619,45 @@ declare namespace Eris { unsendMessage(messageID: string): Promise; } + export class Entitlement extends Base { + applicationID: string; + consumed: boolean; + deleted: boolean; + endsAt?: number | null; + guildID?: string; + skuID: string; + startsAt?: number | null; + type: EntitlementTypes; + userID?: string; + consume(): Promise; + } + + export class SKU extends Base { + applicationID: string; + flags: SKUFlags; + name: string; + slug: string; + type: SKUTypes; + createTestEntitlement(ownerID: string, ownerType: EntitlementOwnerTypes): Promise; + getEntitlements(options?: Omit): Promise; + getSKUSubscription(subscriptionID: string): Promise; + getSKUSubscriptions(): Promise; + } + + export class Subscription extends Base { + canceledAt: number | null; + country?: string; + currentPeriodEnd: number; + + currentPeriodStart: number; + + entitlementIDs: string[]; + renewalSKUIDs: string[] | null; + skuIDs: string[]; + status: SubscriptionStatuses; + userID: string; + } + export class ExtendedUser extends User { email: string; mfaEnabled: boolean; @@ -2681,6 +2772,8 @@ declare namespace Eris { createSoundboardSound(sound: GuildSoundboardSoundCreate, reason?: string): Promise; createSticker(options: CreateStickerOptions, reason?: string): Promise; createTemplate(name: string, description?: string | null): Promise; + createTestEntitlement(skuID: string): Promise; + delete(): Promise; deleteAutoModerationRule(ruleID: string, reason?: string): Promise; deleteCommand(commandID: string): Promise; @@ -2733,6 +2826,8 @@ declare namespace Eris { getDiscovery(): Promise; /** @deprecated */ getEmbed(): Promise; + getEntitlements(options?: Omit): Promise; + getIntegrations(): Promise; getInvites(): Promise; getOnboarding(): Promise; @@ -3527,9 +3622,12 @@ declare namespace Eris { system: boolean; username: string; constructor(data: BaseData, client: Client); + createTestEntitlement(skuID: string): Promise; dynamicAvatarURL(format?: ImageFormat, size?: number): string; dynamicBannerURL(format?: ImageFormat, size?: number): string | null; + getDMChannel(): Promise; + getEntitlements(options?: Omit): Promise; } export class VoiceChannel extends GuildTextableChannel implements Invitable, Permissionable { diff --git a/index.js b/index.js index b2376c41c..7f1c0a43f 100644 --- a/index.js +++ b/index.js @@ -23,6 +23,7 @@ Eris.Constants = require("./lib/Constants"); Eris.DiscordHTTPError = require("./lib/errors/DiscordHTTPError"); Eris.DiscordRESTError = require("./lib/errors/DiscordRESTError"); Eris.DMChannel = require("./lib/structures/DMChannel"); +Eris.Entitlement = require("./lib/structures/Entitlement"); Eris.ExtendedUser = require("./lib/structures/ExtendedUser"); Eris.ForumChannel = require("./lib/structures/ForumChannel"); Eris.GroupChannel = require("./lib/structures/GroupChannel"); @@ -49,6 +50,8 @@ Eris.PrivateThreadChannel = require("./lib/structures/PrivateThreadChannel"); Eris.PublicThreadChannel = require("./lib/structures/PublicThreadChannel"); Eris.RequestHandler = require("./lib/rest/RequestHandler"); Eris.Role = require("./lib/structures/Role"); +Eris.SKU = require("./lib/structures/SKU"); +Eris.Subscription = require("./lib/structures/Subscription"); Eris.SequentialBucket = require("./lib/util/SequentialBucket"); Eris.Shard = require("./lib/gateway/Shard"); Eris.SharedStream = require("./lib/voice/SharedStream"); diff --git a/lib/Client.js b/lib/Client.js index 97f9ea634..cd698daa7 100644 --- a/lib/Client.js +++ b/lib/Client.js @@ -30,6 +30,8 @@ const ThreadMember = require("./structures/ThreadMember"); const UnavailableGuild = require("./structures/UnavailableGuild"); const User = require("./structures/User"); const VoiceConnectionManager = require("./voice/VoiceConnectionManager"); +const Entitlement = require("./structures/Entitlement"); +const Subscription = require("./structures/Subscription"); const VoiceState = require("./structures/VoiceState"); let EventEmitter; @@ -516,6 +518,15 @@ class Client extends EventEmitter { } } + /** + * For One-Time Purchase consumable SKUs, marks a given entitlement for the user as consumed + * @arg {String} entitlementID The ID of the the entitlement + * @returns {Promise} + */ + consumeEntitlement(entitlementID) { + return this.requestHandler.request("POST", Endpoints.ENTITLEMENT_CONSUME(this.application.id, entitlementID), true); + } + /** * Create an auto moderation rule * @arg {String} guildID the ID of the guild to create the rule in @@ -1075,6 +1086,22 @@ class Client extends EventEmitter { }).then((instance) => new StageInstance(instance, this)); } + /** + * Create a test entitlement + * @arg {Object} options The options for creating a test entitlement + * @arg {String} options.skuID The ID of the SKU to grant the entitlement to + * @arg {String} options.ownerID The ID of the owner to create the entitlement for + * @arg {Number} options.ownerType The type of the owner to create the entitlement for + * @returns {Promise} + */ + createTestEntitlement(options) { + return this.requestHandler.request("POST", Endpoints.ENTITLEMENTS(this.application.id), true, { + sku_id: options.skuID, + owner_id: options.ownerID, + owner_type: options.ownerType, + }).then((entitlement) => new Entitlement(entitlement, this)); + } + /** * Create a thread in a channel * @arg {String} channelID The ID of the channel @@ -1440,6 +1467,15 @@ class Client extends EventEmitter { return this.requestHandler.request("DELETE", Endpoints.STAGE_INSTANCE(channelID), true); } + /** + * Delete a test entitlement + * @arg {String} entitlementID The ID of the entitlement + * @returns {Promise} + */ + deleteTestEntitlement(entitlementID) { + return this.requestHandler.request("DELETE", Endpoints.ENTITLEMENT(this.application.id, entitlementID), true); + } + /** * Delete a webhook * @arg {String} webhookID The ID of the webhook @@ -2675,6 +2711,30 @@ class Client extends EventEmitter { return this.requestHandler.request("GET", Endpoints.CUSTOM_EMOJI_GUILD(emojiID), true).then((result) => new Guild(result, this)); } + /** + * Get a list of entitlements for this application + * @arg {Object} [options] The options for the request + * @arg {String} [options.userID] The user ID to look up entitlements for + * @arg {Array} [options.skuIDs] An optional list of SKU IDs to check entitlements for + * @arg {Number} [options.before] Retrieve entitlements before this entitlement ID + * @arg {Number} [options.after] Retrieve entitlements after this entitlement ID + * @arg {Number} [options.limit=100] The number of entitlements to return, 1-100, default 100 + * @arg {String} [options.guildID] The guild ID to look up entitlements for + * @arg {Boolean} [options.excludeEnded] Whether or not ended entitlements should be omitted + * @returns {Promise>} + */ + getEntitlements(options = {}) { + return this.requestHandler.request("GET", Endpoints.ENTITLEMENTS(this.application.id), true, { + user_id: options.userID, + sku_ids: options.skuIDs, + before: options.before, + after: options.after, + limit: options.limit, + guild_id: options.guildID, + exclude_ended: options.excludeEnded, + }).then((entitlements) => entitlements.map((entitlement) => new Entitlement(entitlement, this))); + } + /** * Get info on connecting to the Discord gateway * @returns {Promise} Resolves with an object containing gateway connection info @@ -3459,6 +3519,43 @@ class Client extends EventEmitter { return this.requestHandler.request("GET", Endpoints.USER("@me"), true).then((data) => new ExtendedUser(data, this)); } + /** + * Get a list of SKUs for a given application + * @returns {Promise>} + */ + getSKUs() { + return this.requestHandler.request("GET", Endpoints.SKUS(this.application.id), true); + } + + /** + * Get a subscription by its ID from an SKU + * @arg {String} [skuID] The id of the sku + * @arg {String} [subscriptionID] The id of the subscription + * @returns {Promise} + */ + getSKUSubscription(skuID, subscriptionID) { + return this.requestHandler.request("GET", Endpoints.SKU_SUBSCRIPTION(skuID, subscriptionID), true).then((subscription) => new Subscription(subscription)); + } + + /** + * Get a list of subscriptions from an SKU + * @arg {String} [skuID] The id of the sku + * @arg {Object} [options] The options for the request + * @arg {String} [options.userID] The user ID to look up subscriptions for. Required except for OAuth queries. + * @arg {Number} [options.before] Retrieve subscriptions before this ID + * @arg {Number} [options.after] Retrieve subscriptions after this ID + * @arg {Number} [options.limit=50] The number of subscriptions to return, 1-100, default 50 + * @returns {Promise>} + */ + getSKUSubscriptions(skuID, options = {}) { + return this.requestHandler.request("GET", Endpoints.SKU_SUBSCRIPTIONS(skuID), true, { + user_id: options.userID, + before: options.before, + after: options.after, + limit: options.limit, + }).then((subscriptions) => subscriptions.map((subscription) => new Subscription(subscription))); + } + /** * Get the default soundboard sounds * @returns {Promise>} diff --git a/lib/Constants.d.ts b/lib/Constants.d.ts index 402157741..36fb6a88e 100644 --- a/lib/Constants.d.ts +++ b/lib/Constants.d.ts @@ -149,6 +149,7 @@ export default interface Constants { SUCCESS: 3; DANGER: 4; LINK: 5; + PREMIUM: 6; }; ChannelFlags: { PINNED: 1; @@ -196,6 +197,20 @@ export default interface Constants { LATEST_ACTIVITY: 0; CREATION_DATE: 1; }; + EntitlementTypes: { + PURCHASE: 1; + PREMIUM_SUBSCRIPTION: 2; + DEVELOPER_GIFT: 3; + TEST_MODE_PURCHASE: 4; + FREE_PURCHASE: 5; + USER_GIFT: 6; + PREMIUM_PURCHASE: 7; + APPLICATION_SUBSCRIPTION: 8; + }; + EntitlementOwnerTypes: { + GUILD: 1; + USER: 2; + }; ExplicitContentFilterLevels: { DISABLED: 0; MEMBERS_WITHOUT_ROLES: 1; @@ -615,6 +630,17 @@ export default interface Constants { NORMAL: 0; BURST: 1; }; + SKUTypes: { + DURABLE: 2; + CONSUMABLE: 3; + SUBSCRIPTION: 5; + SUBSCRIPTION_GROUP: 6; + }; + SKUFlags: { + AVAILABLE: 4; + GUILD_SUBSCRIPTION: 128; + USER_SUBSCRIPTION: 256; + }; StageInstancePrivacyLevel: { PUBLIC: 1; GUILD_ONLY: 2; @@ -652,6 +678,11 @@ export default interface Constants { "Good to see you, %user%.", "Yay you made it, %user%!", ]; + SubscriptionStatuses: { + ACTIVE: 0; + ENDING: 1; + INACTIVE: 2; + }; ThreadMemberFlags: { HAS_INTERACTED: 1; ALL_MESSAGES: 2; diff --git a/lib/Constants.js b/lib/Constants.js index c0de89331..dcb809207 100644 --- a/lib/Constants.js +++ b/lib/Constants.js @@ -174,6 +174,7 @@ module.exports.ButtonStyles = { SUCCESS: 3, DANGER: 4, LINK: 5, + PREMIUM: 6, }; module.exports.ChannelFlags = { @@ -233,6 +234,22 @@ module.exports.SortOrderTypes = { CREATION_DATE: 1, }; +module.exports.EntitlementTypes = { + PURCHASE: 1, + PREMIUM_SUBSCRIPTION: 2, + DEVELOPER_GIFT: 3, + TEST_MODE_PURCHASE: 4, + FREE_PURCHASE: 5, + USER_GIFT: 6, + PREMIUM_PURCHASE: 7, + APPLICATION_SUBSCRIPTION: 8, +}; + +module.exports.EntitlementOwnerTypes = { + GUILD: 1, + USER: 2, +}; + module.exports.ExplicitContentFilterLevels = { DISABLED: 0, MEMBERS_WITHOUT_ROLES: 1, @@ -771,6 +788,19 @@ module.exports.RoleFlags = { IN_PROMPT: 1 << 0, }; +module.exports.SKUTypes = { + DURABLE: 2, + CONSUMABLE: 3, + SUBSCRIPTION: 5, + SUBSCRIPTION_GROUP: 6, +}; + +module.exports.SKUFlags = { + AVAILABLE: 1 << 2, + GUILD_SUBSCRIPTION: 1 << 7, + USER_SUBSCRIPTION: 1 << 8, +}; + module.exports.StageInstancePrivacyLevel = { PUBLIC: 1, GUILD_ONLY: 2, @@ -813,6 +843,12 @@ module.exports.SystemJoinMessages = [ "Yay you made it, %user%!", ]; +module.exports.SubscriptionStatuses = { + ACTIVE: 0, + ENDING: 1, + INACTIVE: 2, +}; + module.exports.ThreadMemberFlags = { HAS_INTERACTED: 1 << 0, ALL_MESSAGES: 1 << 1, diff --git a/lib/gateway/Shard.js b/lib/gateway/Shard.js index 32882ccf6..ff2b01bdd 100644 --- a/lib/gateway/Shard.js +++ b/lib/gateway/Shard.js @@ -19,6 +19,8 @@ const GuildScheduledEvent = require("../structures/GuildScheduledEvent"); const GuildAuditLogEntry = require("../structures/GuildAuditLogEntry"); const AutoModerationRule = require("../structures/AutoModerationRule"); const SoundboardSound = require("../structures/SoundboardSound"); +const Entitlement = require("../structures/Entitlement"); +const Subscription = require("../structures/Subscription"); const WebSocket = typeof window !== "undefined" ? require("../util/BrowserWebSocket") : require("ws"); @@ -2601,6 +2603,60 @@ class Shard extends EventEmitter { } break; } + case "ENTITLEMENT_CREATE": { + /** + * Fired when an entitlement is created + * @event Client#entitlementCreate + * @prop {Entitlement} entitlement The new entitlement + */ + this.emit("entitlementCreate", new Entitlement(packet.d, this.client)); + break; + } + case "ENTITLEMENT_UPDATE": { + /** + * Fired when an entitlement is updated + * @event Client#entitlementUpdate + * @prop {Entitlement} entitlement The updated entitlement + */ + this.emit("entitlementUpdate", new Entitlement(packet.d, this.client)); + break; + } + case "ENTITLEMENT_DELETE": { + /** + * Fired when an entitlement is deleted + * @event Client#entitlementDelete + * @prop {Entitlement} entitlement The deleted entitlement + */ + this.emit("entitlementDelete", new Entitlement(packet.d, this.client)); + break; + } + case "SUBSCRIPTION_CREATE": { + /** + * Fired when an subscription is created + * @event Client#subscriptionCreate + * @prop {Subscription} subscription The new subscription + */ + this.emit("subscriptionCreate", new Subscription(packet.d)); + break; + } + case "SUBSCRIPTION_UPDATE": { + /** + * Fired when an subscription is updated + * @event Client#subscriptionUpdate + * @prop {Subscription} subscription The updated subscription + */ + this.emit("subscriptionUpdate", new Subscription(packet.d)); + break; + } + case "SUBSCRIPTION_DELETE": { + /** + * Fired when an subscription is deleted + * @event Client#subscriptionDelete + * @prop {Subscription} subscription The deleted subscription + */ + this.emit("subscriptionDelete", new Subscription(packet.d)); + break; + } default: { /** * Fired when the shard encounters an unknown packet diff --git a/lib/rest/Endpoints.js b/lib/rest/Endpoints.js index ca49402a4..1526868b9 100644 --- a/lib/rest/Endpoints.js +++ b/lib/rest/Endpoints.js @@ -35,6 +35,9 @@ module.exports.CHANNELS = module.exports.CUSTOM_EMOJI_GUILD = (emojiID) => `/emojis/${emojiID}/guild`; module.exports.DISCOVERY_CATEGORIES = "/discovery/categories"; module.exports.DISCOVERY_VALIDATION = "/discovery/valid-term"; +module.exports.ENTITLEMENT = (applicationID, entitlementID) => `/applications/${applicationID}/entitlements/${entitlementID}`; +module.exports.ENTITLEMENT_CONSUME = (applicationID, entitlementID) => `/applications/${applicationID}/entitlements/${entitlementID}/consume`; +module.exports.ENTITLEMENTS = (applicationID) => `/applications/${applicationID}/entitlements`; module.exports.GATEWAY = "/gateway"; module.exports.GATEWAY_BOT = "/gateway/bot"; module.exports.GUILD = (guildID) => `/guilds/${guildID}`; @@ -89,6 +92,9 @@ module.exports.ORIGINAL_INTERACTION_RESPONSE = (appID, interactToken) module.exports.POLL_ANSWER_VOTERS = (channelID, msgID, answerID) => `/channels/${channelID}/polls/${msgID}/answers/${answerID}`; module.exports.POLL_END = (channelID, msgID) => `/channels/${channelID}/polls/${msgID}/expire`; module.exports.ROLE_CONNECTION_METADATA_RECORDS = (appID) => `/applications/${appID}/role-connections/metadata`; +module.exports.SKUS = (applicationID) => `/applications/${applicationID}/skus`; +module.exports.SKU_SUBSCRIPTION = (skuID, subscriptionID) => `/skus/${skuID}/subscriptions/${subscriptionID}`; +module.exports.SKU_SUBSCRIPTIONS = (skuID) => `/skus/${skuID}/subscriptions`; module.exports.SOUNDBOARD_SOUND_GUILD = (guildID, soundID) => `/guilds/${guildID}/soundboard-sounds/${soundID}`; module.exports.SOUNDBOARD_SOUNDS_DEFAULT = "/soundboard-default-sounds"; module.exports.SOUNDBOARD_SOUNDS_GUILD = (guildID) => `/guilds/${guildID}/soundboard-sounds`; diff --git a/lib/structures/Entitlement.js b/lib/structures/Entitlement.js new file mode 100644 index 000000000..e70a8b91c --- /dev/null +++ b/lib/structures/Entitlement.js @@ -0,0 +1,47 @@ +"use strict"; + +const Base = require("./Base.js"); + +/** + * Represents an entitlement + * @prop {String} applicationID The ID of the parent application + * @prop {Boolean} consumed For consumable items, whether or not the entitlement has been consumed + * @prop {Boolean} deleted Whether or not the entitlement was deleted + * @prop {Number?} endsAt The date at which the entitlement is no longer valid. Not present when using test entitlements + * @prop {String?} guildID The ID of the guild that is granted access to the entitlement's sku + * @prop {String} skuID The ID of the SKU + * @prop {Number?} startsAt The start date at which the entitlement is valid. Not present when using test entitlements + * @prop {Number} type The type of entitlement + * @prop {String?} userID The ID of the user that is granted access to the entitlement's sku + */ +class Entitlement extends Base { + constructor(data, client) { + super(data.id); + this._client = client; + + this.applicationID = data.application_id; + this.consumed = data.consumed; + this.deleted = data.deleted; + this.guildID = data.guild_id; + this.skuID = data.sku_id; + this.type = data.type; + this.userID = data.user_id; + + if (data.ends_at !== undefined) { + this.endsAt = data.ends_at ? Date.parse(data.ends_at) : null; + } + if (data.starts_at !== undefined) { + this.startsAt = data.starts_at ? Date.parse(data.starts_at) : null; + } + } + + /** + * Marks this entitlement as consumed + * @returns {Promise} + */ + consume() { + return this._client.consumeEntitlement.call(this._client, this.id); + } +} + +module.exports = Entitlement; diff --git a/lib/structures/Guild.js b/lib/structures/Guild.js index bde1ab571..08fe2967f 100644 --- a/lib/structures/Guild.js +++ b/lib/structures/Guild.js @@ -10,7 +10,7 @@ const Role = require("./Role"); const VoiceState = require("./VoiceState"); const Permission = require("./Permission"); const GuildScheduledEvent = require("./GuildScheduledEvent"); -const { Permissions } = require("../Constants"); +const { Permissions, EntitlementOwnerTypes } = require("../Constants"); const StageInstance = require("./StageInstance"); const ThreadChannel = require("./ThreadChannel"); const AutoModerationRule = require("./AutoModerationRule"); @@ -594,6 +594,19 @@ class Guild extends Base { return this._client.createGuildTemplate.call(this._client, this.id, name, description); } + /** + * Create a test entitlement for this guild + * @arg {String} skuID The ID of the SKU to grant the entitlement to + * @returns {Promise} + */ + createTestEntitlement(skuID) { + return this._client.createTestEntitlement.call(this._client, { + skuID: skuID, + ownerID: this.id, + ownerType: EntitlementOwnerTypes.GUILD, + }); + } + /** * Delete the guild (bot user must be owner) * @returns {Promise} @@ -1167,6 +1180,24 @@ class Guild extends Base { return this._client.getGuildDiscovery.call(this._client, this.id); } + /** + * Get a list of entitlements for this guild + * @arg {Object} [options] The options for the request + * @arg {String} [options.userID] The user ID to look up entitlements for + * @arg {Array} [options.skuIDs] An optional list of SKU IDs to check entitlements for + * @arg {Number} [options.before] Retrieve entitlements before this entitlement ID + * @arg {Number} [options.after] Retrieve entitlements after this entitlement ID + * @arg {Number} [options.limit=100] The number of entitlements to return, 1-100, default 100 + * @arg {Boolean} [options.excludeEnded] Whether or not ended entitlements should be omitted + * @returns {Promise>} + */ + getEntitlements(options = {}) { + return this._client.getEntitlements.call(this._client, { + guildID: this.id, + ...options, + }); + } + /** * Get the all of a guild's application command permissions * @returns {Promise>} Resolves with an array of guild application command permissions objects. diff --git a/lib/structures/SKU.js b/lib/structures/SKU.js new file mode 100644 index 000000000..767404647 --- /dev/null +++ b/lib/structures/SKU.js @@ -0,0 +1,80 @@ +"use strict"; + +const Base = require("./Base.js"); + +/** + * Represents an SKU + * @prop {String} applicationID The ID of the parent application + * @prop {Number} flags The SKU flags combined as a bitfield + * @prop {String} name The name of the SKU + * @prop {String} slug System-generated URL slug based on the SKU's name + * @prop {Number} type The type of SKU + */ +class SKU extends Base { + constructor(data, client) { + super(data.id); + this._client = client; + + this.applicationID = data.application_id; + this.flags = data.flags; + this.name = data.name; + this.slug = data.slug; + this.type = data.type; + } + + /** + * Create a test entitlement for this SKU + * @arg {String} ownerID The ID of the owner to create the entitlement for + * @arg {Number} ownerType The type of the owner to create the entitlement for + * @returns {Promise} + */ + createTestEntitlement(ownerID, ownerType) { + return this._client.createTestEntitlement.call(this._client, { + ownerID: ownerID, + ownerType: ownerType, + skuID: this.id, + }); + } + + /** + * Get a list of entitlements for this SKU + * @arg {Object} [options] The options for the request + * @arg {String} [options.userID] The user ID to look up entitlements for + * @arg {Number} [options.before] Retrieve entitlements before this entitlement ID + * @arg {Number} [options.after] Retrieve entitlements after this entitlement ID + * @arg {Number} [options.limit=100] The number of entitlements to return, 1-100, default 100 + * @arg {String} [options.guildID] The guild ID to look up entitlements for + * @arg {Boolean} [options.excludeEnded] Whether or not ended entitlements should be omitted + * @returns {Promise>} + */ + getEntitlements(options = {}) { + return this._client.getEntitlements.call(this._client, { + skuIDs: [this.id], + ...options, + }); + } + + /** + * Get a subscription by its ID from this SKU + * @arg {String} [subscriptionID] The id of the subscription + * @returns {Promise} + */ + getSKUSubscription(subscriptionID) { + return this._client.getSKUSubscription.call(this._client, this.id, subscriptionID); + } + + /** + * Get a list of subscriptions containing this SKU, filtered by user. + * @arg {Object} [options] The options for the request + * @arg {String} [options.userID] The user ID to look up subscriptions for. Required except for OAuth queries. + * @arg {Number} [options.before] Retrieve subscriptions before this ID + * @arg {Number} [options.after] Retrieve subscriptions after this ID + * @arg {Number} [options.limit=50] The number of subscriptions to return, 1-100, default 50 + * @returns {Promise>} + */ + getSKUSubscriptions(options = {}) { + return this._client.getSKUSubscriptions.call(this._client, this.id, options); + } +} + +module.exports = SKU; diff --git a/lib/structures/Subscription.js b/lib/structures/Subscription.js new file mode 100644 index 000000000..a79903a83 --- /dev/null +++ b/lib/structures/Subscription.js @@ -0,0 +1,37 @@ +"use strict"; + +const Base = require("./Base.js"); + +/** + * Represents a subscription + * @prop {Number?} canceledAt When the subscription was canceled + * @prop {Number?} currentPeriodEnd The end date of the current subscription period + * @prop {Number?} currentPeriodStart The start date of the current subscription period + * @prop {String?} country ISO3166-1 alpha-2 country code of the payment source used to purchase the subscription. Missing unless queried with a private OAuth scope. + * @prop {Array?} renewalSKUIDs A list of SKUs that this user will be subscribed to at renewal + * @prop {Array} entitlementIDs A list of entitlements granted for this subscription + * @prop {Array} skuIDs A list of SKUs subscribed to + * @prop {String} status The current status of the subscription + * @prop {String} userID The ID of the user who is subscribed + */ +class Subscription extends Base { + constructor(data) { + super(data.id); + + this.country = data.country; + this.renewalSKUIDs = data.renewal_sku_ids; + this.entitlementIDs = data.entitlement_ids; + this.skuIDs = data.sku_ids; + this.status = data.status; + this.userID = data.user_id; + + this.currentPeriodEnd = data.current_period_end ? Date.parse(data.current_period_end) : null; + this.currentPeriodStart = data.current_period_start ? Date.parse(data.current_period_start) : null; + + if (data.canceled_at !== undefined) { + this.canceledAt = data.canceled_at ? Date.parse(data.canceled_at) : null; + } + } +} + +module.exports = Subscription; diff --git a/lib/structures/User.js b/lib/structures/User.js index 524f30b20..aef335bdf 100644 --- a/lib/structures/User.js +++ b/lib/structures/User.js @@ -2,6 +2,7 @@ const Base = require("./Base"); const Endpoints = require("../rest/Endpoints"); +const { EntitlementOwnerTypes } = require("../Constants"); /** * Represents a user @@ -110,6 +111,19 @@ class User extends Base { return this.avatar ? this._client._formatImage(Endpoints.USER_AVATAR(this.id, this.avatar), "jpg") : this.defaultAvatarURL; } + /** + * Create a test entitlement for this user + * @arg {String} skuID The ID of the SKU to grant the entitlement to + * @returns {Promise} + */ + createTestEntitlement(skuID) { + return this._client.createTestEntitlement.call(this._client, { + skuID: skuID, + ownerID: this.id, + ownerType: EntitlementOwnerTypes.USER, + }); + } + /** * Get the user's avatar with the given format and size * @arg {String} [format] The filetype of the avatar ("jpg", "jpeg", "png", "gif", or "webp") @@ -150,6 +164,24 @@ class User extends Base { return this._client.getDMChannel.call(this._client, this.id); } + /** + * Get a list of entitlements for this user + * @arg {Object} [options] The options for the request + * @arg {Array} [options.skuIDs] An optional list of SKU IDs to check entitlements for + * @arg {Number} [options.before] Retrieve entitlements before this entitlement ID + * @arg {Number} [options.after] Retrieve entitlements after this entitlement ID + * @arg {Number} [options.limit=100] The number of entitlements to return, 1-100, default 100 + * @arg {String} [options.guildID] The guild ID to look up entitlements for + * @arg {Boolean} [options.excludeEnded] Whether or not ended entitlements should be omitted + * @returns {Promise>} + */ + getEntitlements(options = {}) { + return this._client.getEntitlements.call(this._client, { + userID: this.id, + ...options, + }); + } + toJSON(props = []) { return super.toJSON([ "accentColor",