Skip to content

Commit

Permalink
feat: premium app subscriptions (#114)
Browse files Browse the repository at this point in the history
Probably alright to the degree that I could test (I don't have access to monetization features).

Ref:
discord/discord-api-docs@b97fad3
Ref:
discord/discord-api-docs@70b03d1
Ref:
discord/discord-api-docs@b8feadd
Ref: discord/discord-api-docs#6502
Ref: discord/discord-api-docs#6548
Ref:
discord/discord-api-docs@f0d1615
  • Loading branch information
TTtie authored Aug 28, 2024
1 parent 8ab6bbf commit 4610478
Show file tree
Hide file tree
Showing 13 changed files with 368 additions and 8 deletions.
1 change: 1 addition & 0 deletions esm.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const {
Constants,
DiscordHTTPError,
DiscordRESTError,
Entitlement,
ExtendedUser,
ForumChannel,
Guild,
Expand Down
105 changes: 100 additions & 5 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -144,10 +144,11 @@ declare namespace Dysnomia {
// Message
type ActionRowComponents = Button | SelectMenu;
type BaseSelectMenuTypes = Exclude<SelectMenuTypes, SelectMenuExtendedTypes>;
type Button = InteractionButton | URLButton;
type Button = InteractionButton | PremiumButton | URLButton;
type ButtonStyles = Constants["ButtonStyles"][keyof Constants["ButtonStyles"]];
type ButtonStyleNormal = Exclude<ButtonStyles, ButtonStyleLink>;
type ButtonStyleNormal = Exclude<ButtonStyles, ButtonStyleLink | ButtonStylePremium>;
type ButtonStyleLink = Constants["ButtonStyles"]["LINK"];
type ButtonStylePremium = Constants["ButtonStyles"]["PREMIUM"];
type Component = ActionRow | ActionRowComponents;
type ComponentTypes = Constants["ComponentTypes"][keyof Constants["ComponentTypes"]];
type ImageFormat = Constants["ImageFormats"][number];
Expand Down Expand Up @@ -197,6 +198,11 @@ declare namespace Dysnomia {
type MessageWebhookContent = Pick<WebhookPayload, "content" | "embeds" | "allowedMentions" | "components" | "attachments" | "threadID">;
type WebhookTypes = Constants["WebhookTypes"][keyof Constants["WebhookTypes"]];

// Subscriptions
type EntitlementOwnerTypes = Constants["EntitlementOwnerTypes"][keyof Constants["EntitlementOwnerTypes"]];
type EntitlementTypes = Constants["EntitlementTypes"][keyof Constants["EntitlementTypes"]];
type SKUTypes = Constants["SKUTypes"][keyof Constants["SKUTypes"]];

// INTERFACES
// Internals
interface JSONCache {
Expand Down Expand Up @@ -736,6 +742,9 @@ declare namespace Dysnomia {
connect: [id: number];
debug: [message: string, id?: number];
disconnect: [];
entitlementCreate: [entitlement: Entitlement];
entitlementUpdate: [entitlement: Entitlement];
entitlementDelete: [entitlement: Entitlement];
error: [err: Error, id?: number];
guildAuditLogEntryCreate: [entry: GuildAuditLogEntry];
guildAvailable: [guild: Guild];
Expand Down Expand Up @@ -1249,7 +1258,7 @@ declare namespace Dysnomia {
}
interface InteractionResponseMessage {
data: RawInteractionContent;
type: Constants["InteractionResponseTypes"]["CHANNEL_MESSAGE_WITH_SOURCE" | "UPDATE_MESSAGE"];
type: Constants["InteractionResponseTypes"]["CHANNEL_MESSAGE_WITH_SOURCE" | "UPDATE_MESSAGE" | "PREMIUM_REQUIRED"];
}
interface InteractionResponseModal {
data: InteractionModalContent;
Expand Down Expand Up @@ -1398,8 +1407,6 @@ declare namespace Dysnomia {
}
interface ButtonBase {
disabled?: boolean;
emoji?: PartialEmoji;
label?: string;
type: Constants["ComponentTypes"]["BUTTON"];
}
interface ChannelSelectMenu extends SelectMenuBase {
Expand Down Expand Up @@ -1483,6 +1490,8 @@ declare namespace Dysnomia {

interface InteractionButton extends ButtonBase {
custom_id: string;
emoji?: PartialEmoji;
label?: string;
style: ButtonStyleNormal;
}
interface MessageActivity {
Expand Down Expand Up @@ -1563,6 +1572,11 @@ declare namespace Dysnomia {
is_finalized: boolean;
answer_counts: PollAnswerCount[];
}
interface PremiumButton extends ButtonBase {
sku_id: string;
style: Constants["ButtonStyles"]["PREMIUM"];

}
interface RoleSubscriptionData {
isRenewal: boolean;
roleSubscriptionListingID: string;
Expand Down Expand Up @@ -1597,6 +1611,8 @@ declare namespace Dysnomia {
}
interface URLButton extends ButtonBase {
style: Constants["ButtonStyles"]["LINK"];
emoji?: PartialEmoji;
label?: string;
url: string;
}

Expand Down Expand Up @@ -2071,6 +2087,7 @@ declare namespace Dysnomia {
SUCCESS: 3;
DANGER: 4;
LINK: 5;
PREMIUM: 6;
};
ChannelTypes: {
GUILD_TEXT: 0;
Expand Down Expand Up @@ -2111,6 +2128,20 @@ declare namespace Dysnomia {
ALL_MESSAGES: 0;
ONLY_MENTIONS: 1;
};
EntitlementOwnerTypes: {
GUILD: 1;
USER: 2;
};
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;
};
ExplicitContentFilterLevels: {
DISABLED: 0;
MEMBERS_WITHOUT_ROLES: 1;
Expand Down Expand Up @@ -2227,6 +2258,8 @@ declare namespace Dysnomia {
UPDATE_MESSAGE: 7;
APPLICATION_COMMAND_AUTOCOMPLETE_RESULT: 8;
MODAL: 9;
/** @deprecated */
PREMIUM_REQUIRED: 10;
};
InteractionTypes: {
PING: 1;
Expand Down Expand Up @@ -2420,6 +2453,17 @@ declare namespace Dysnomia {
RoleFlags: {
IN_PROMPT: 1;
};
SKUFlags: {
AVAILABLE: 4;
GUILD_SUBSCRIPTION: 128;
USER_SUBSCRIPTION: 256;
};
SKUTypes: {
DURABLE: 2;
CONSUMABLE: 3;
SUBSCRIPTION: 5;
SUBSCRIPTION_GROUP: 6;
};
StageInstancePrivacyLevel: {
/** @deprecated */
PUBLIC: 1;
Expand Down Expand Up @@ -2543,6 +2587,32 @@ declare namespace Dysnomia {
}
/* eslint-enable @stylistic/key-spacing, @stylistic/no-multi-spaces */

// Subscriptions
interface CreateTestEntitlementOptions {
skuID: string;
ownerID: string;
ownerType: EntitlementOwnerTypes;
}

interface GetEntitlementsOptions {
after?: number;
before?: number;
excludeEnded?: boolean;
guildID?: string;
limit?: number;
skuIDs?: string[];
userID?: string;
}

interface SKU {
id: string;
application_id: string;
type: SKUTypes;
name: string;
slug: string;
flags: number;
}

// Classes
export class AutocompleteInteraction<T extends PossiblyUncachedInteractionChannel = TextableChannel> extends Interaction {
appPermissions?: Permission;
Expand Down Expand Up @@ -2683,6 +2753,7 @@ declare namespace Dysnomia {
bulkEditGuildCommands(guildID: string, commands: ApplicationCommandStructure[]): Promise<AnyApplicationCommand<true>[]>;
closeVoiceConnection(guildID: string): void;
connect(): Promise<void>;
consumeEntitlement(entitlementID: string): Promise<void>;
createApplicationEmoji(options: EmojiOptions): Promise<Emoji>;
createAutoModerationRule(guildID: string, rule: CreateAutoModerationRuleOptions): Promise<AutoModerationRule>;
createChannel(guildID: string, name: string): Promise<TextChannel>;
Expand Down Expand Up @@ -2757,6 +2828,7 @@ declare namespace Dysnomia {
createRole(guildID: string, options?: RoleOptions, reason?: string): Promise<Role>;
createRole(guildID: string, options?: Role, reason?: string): Promise<Role>;
createStageInstance(channelID: string, options: CreateStageInstanceOptions): Promise<StageInstance>;
createTestEntitlement(options: CreateTestEntitlementOptions): Promise<Entitlement>;
createThread(channelID: string, options: CreateThreadWithoutMessageOptions): Promise<ThreadChannel>;
createThreadWithMessage(channelID: string, messageID: string, options: CreateThreadOptions): Promise<NewsThreadChannel | PublicThreadChannel>;
crosspostMessage(channelID: string, messageID: string): Promise<Message>;
Expand All @@ -2777,6 +2849,7 @@ declare namespace Dysnomia {
deleteMessages(channelID: string, messageIDs: string[], reason?: string): Promise<void>;
deleteRole(guildID: string, roleID: string, reason?: string): Promise<void>;
deleteStageInstance(channelID: string): Promise<void>;
deleteTestEntitlement(entitlementID: string): Promise<void>;
deleteWebhook(webhookID: string, token?: string, reason?: string): Promise<void>;
deleteWebhookMessage(webhookID: string, token: string, messageID: string, threadID?: string): Promise<void>;
disconnect(options: { reconnect?: boolean | "auto" }): void;
Expand Down Expand Up @@ -2863,6 +2936,7 @@ declare namespace Dysnomia {
getCommandPermissions(guildID: string, commandID: string): Promise<GuildApplicationCommandPermissions>;
getCommands<W extends boolean = false>(withLocalizations?: W): Promise<AnyApplicationCommand<W>[]>;
getDMChannel(userID: string): Promise<PrivateChannel>;
getEntitlements(options?: GetEntitlementsOptions): Promise<Entitlement[]>;
getGateway(): Promise<{ url: string }>;
getGuildAuditLog(guildID: string, options?: GetGuildAuditLogOptions): Promise<GuildAuditLog>;
getGuildBan(guildID: string, userID: string): Promise<GuildBan>;
Expand Down Expand Up @@ -2912,6 +2986,7 @@ declare namespace Dysnomia {
getRESTUser(userID: string): Promise<User>;
getRoleConnectionMetadata(): Promise<ApplicationRoleConnectionMetadata[]>;
getSelf(): Promise<ExtendedUser>;
getSKUs(): Promise<SKU[]>;
getStageInstance(channelID: string): Promise<StageInstance>;
getStickerPack(packID: string): Promise<StickerPack>;
getStickerPacks(): Promise<{ sticker_packs: StickerPack[] }>;
Expand Down Expand Up @@ -2983,6 +3058,8 @@ declare namespace Dysnomia {
editMessage(messageID: string, content: string | InteractionContentEdit): Promise<Message>;
editOriginalMessage(content: string | InteractionContentEdit): Promise<Message>;
getOriginalMessage(): Promise<Message>;
/** @deprecated */
requirePremium(): Promise<void>;
}

export class ComponentInteraction<T extends PossiblyUncachedInteractionChannel = TextableChannel> extends Interaction {
Expand All @@ -3006,6 +3083,8 @@ declare namespace Dysnomia {
editOriginalMessage(content: string | InteractionContentEdit): Promise<Message>;
editParent(content: InteractionContentEdit): Promise<void>;
getOriginalMessage(): Promise<Message>;
/** @deprecated */
requirePremium(): Promise<void>;
}

export class DiscordHTTPError extends Error {
Expand All @@ -3030,6 +3109,19 @@ declare namespace Dysnomia {
flattenErrors(errors: HTTPResponse, keyPrefix?: string): string[];
}

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<void>;
}

export class ExtendedUser extends User {
email?: string | null;
mfaEnabled?: boolean;
Expand Down Expand Up @@ -3365,6 +3457,7 @@ declare namespace Dysnomia {
export class Interaction extends Base {
acknowledged: boolean;
applicationID: string;
entitlements: Entitlement[];
id: string;
token: string;
type: number;
Expand Down Expand Up @@ -3526,6 +3619,8 @@ declare namespace Dysnomia {
editOriginalMessage(content: string | InteractionContentEdit): Promise<Message>;
editParent(content: InteractionContentEdit): Promise<void>;
getOriginalMessage(): Promise<Message>;
/** @deprecated */
requirePremium(): Promise<void>;
}

// News channel rate limit is always 0
Expand Down
1 change: 1 addition & 0 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Dysnomia.Collection = require("./lib/util/Collection");
Dysnomia.Constants = require("./lib/Constants");
Dysnomia.DiscordHTTPError = require("./lib/errors/DiscordHTTPError");
Dysnomia.DiscordRESTError = require("./lib/errors/DiscordRESTError");
Dysnomia.Entitlement = require("./lib/structures/Entitlement");
Dysnomia.ExtendedUser = require("./lib/structures/ExtendedUser");
Dysnomia.ForumChannel = require("./lib/structures/ForumChannel");
Dysnomia.Guild = require("./lib/structures/Guild");
Expand Down
59 changes: 59 additions & 0 deletions lib/Client.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const VoiceConnectionManager = require("./voice/VoiceConnectionManager");
const AutoModerationRule = require("./structures/AutoModerationRule");
const emitDeprecation = require("./util/emitDeprecation");
const VoiceState = require("./structures/VoiceState");
const Entitlement = require("./structures/Entitlement");

let EventEmitter;
try {
Expand Down Expand Up @@ -445,6 +446,15 @@ class Client extends EventEmitter {
}
}

/**
* Consumes a one-time purchasable entitlement
* @param {String} entitlementID The ID of the entitlement to consume
* @returns {Promise}
*/
consumeEntitlement(entitlementID) {
return this.requestHandler.request("POST", Endpoints.ENTITLEMENT_CONSUME(this.application.id, entitlementID), true);
}

/**
* Create an application emoji object
* @param {Object} options Emoji options
Expand Down Expand Up @@ -920,6 +930,18 @@ class Client extends EventEmitter {
}).then((instance) => new StageInstance(instance, this));
}

/**
* Creates a testing entitlement for a user/guild
* @param {Object} options The options for this request
* @param {String} options.skuID The ID of the SKU to grant the entitlement to
* @param {String} options.ownerID The ID of the guild or user to grant the entitlement to
* @param {Number} options.ownerType The type of the subscription to grant. `1` for a guild subscription, `2` for a user subscription.
* @returns {Promise<Entitlement>}
*/
createTestEntitlement(options) {
return this.requestHandler.request("POST", Endpoints.ENTITLEMENTS(this.application.id), true, options).then((entitlement) => new Entitlement(entitlement, this));
}

/**
* Create a thread in a channel
* @param {String} channelID The ID of the channel
Expand Down Expand Up @@ -1209,6 +1231,15 @@ class Client extends EventEmitter {
return this.requestHandler.request("DELETE", Endpoints.STAGE_INSTANCE(channelID), true);
}

/**
* Deletes a testing entitlement
* @param {String} entitlementID The test entitlement ID to remove
* @returns {Promise}
*/
deleteTestEntitlement(entitlementID) {
return this.requestHandler.request("DELETE", Endpoints.ENTITLEMENT(this.application.id, entitlementID), true);
}

/**
* Delete a webhook
* @param {String} webhookID The ID of the webhook
Expand Down Expand Up @@ -2268,6 +2299,26 @@ class Client extends EventEmitter {
}).then((privateChannel) => new PrivateChannel(privateChannel, this));
}

/**
* Gets a list of entitlements for this application
* @param {Object} [options] THe options for the request
* @param {String} [options.userID] The user ID to look entitlements up for
* @param {Array<String>} [options.skuIDs] The SKU IDs to look entitlements up for
* @param {Number} [options.before] Look entitlements up before this ID
* @param {Number} [options.after] Look entitlements up after this ID
* @param {Number} [options.limit=100] The amount of entitlements to retrieve (1-100)
* @param {String} [options.guildID] The guild ID to look entitlements up for
* @param {Boolean} [options.excludeEnded] Whether to omit already expired entitlements or not
* @returns {Promise<Array<Entitlement>>}
*/
getEntitlements(options = {}) {
options.user_id = options.userID;
options.sku_ids = options.skuIDs;
options.guild_id = options.guildID;
options.exclude_ended = options.excludeEnded;
return this.requestHandler.request("GET", Endpoints.ENTITLEMENTS(this.application.id), true, options).then((entitlements) => entitlements.map((entitlement) => new Entitlement(entitlement, this)));
}

/**
* Get info on connecting to the Discord gateway
* @returns {Promise<Object>} Resolves with an object containing gateway connection info
Expand Down Expand Up @@ -2942,6 +2993,14 @@ class Client extends EventEmitter {
return this.requestHandler.request("GET", Endpoints.USER("@me"), true).then((data) => new ExtendedUser(data, this));
}

/**
* Gets the list of SKUs associated with the current application
* @returns {Promise<Array<Object>>} An array of [SKU objects](https://discord.com/developers/docs/monetization/skus#sku-object)
*/
getSKUs() {
return this.requestHandler.request("GET", Endpoints.SKUS(this.application.id), true);
}

/**
* Get the stage instance associated with a stage channel
* @param {String} channelID The stage channel ID
Expand Down
Loading

0 comments on commit 4610478

Please sign in to comment.