Skip to content

Commit

Permalink
feat: premium application subscriptions (#9907)
Browse files Browse the repository at this point in the history
* feat: premium application subscriptions

* types: readonly array

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* fix: requested changes

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>

* fix: core client types

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
  • Loading branch information
3 people authored Dec 24, 2023
1 parent 520d6f6 commit c4fcee3
Show file tree
Hide file tree
Showing 33 changed files with 914 additions and 10 deletions.
5 changes: 5 additions & 0 deletions packages/core/src/api/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { ChannelsAPI } from './channel.js';
import { GuildsAPI } from './guild.js';
import { InteractionsAPI } from './interactions.js';
import { InvitesAPI } from './invite.js';
import { MonetizationAPI } from './monetization.js';
import { OAuth2API } from './oauth2.js';
import { RoleConnectionsAPI } from './roleConnections.js';
import { StageInstancesAPI } from './stageInstances.js';
Expand All @@ -20,6 +21,7 @@ export * from './channel.js';
export * from './guild.js';
export * from './interactions.js';
export * from './invite.js';
export * from './monetization.js';
export * from './oauth2.js';
export * from './roleConnections.js';
export * from './stageInstances.js';
Expand All @@ -42,6 +44,8 @@ export class API {

public readonly invites: InvitesAPI;

public readonly monetization: MonetizationAPI;

public readonly oauth2: OAuth2API;

public readonly roleConnections: RoleConnectionsAPI;
Expand All @@ -64,6 +68,7 @@ export class API {
this.channels = new ChannelsAPI(rest);
this.guilds = new GuildsAPI(rest);
this.invites = new InvitesAPI(rest);
this.monetization = new MonetizationAPI(rest);
this.roleConnections = new RoleConnectionsAPI(rest);
this.oauth2 = new OAuth2API(rest);
this.stageInstances = new StageInstancesAPI(rest);
Expand Down
40 changes: 32 additions & 8 deletions packages/core/src/api/interactions.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,16 @@
/* eslint-disable jsdoc/check-param-names */

import type { RawFile, RequestData, REST } from '@discordjs/rest';
import { InteractionResponseType, Routes } from 'discord-api-types/v10';
import type {
APICommandAutocompleteInteractionResponseCallbackData,
APIInteractionResponseCallbackData,
APIModalInteractionResponseCallbackData,
RESTGetAPIWebhookWithTokenMessageResult,
Snowflake,
APIInteractionResponseDeferredChannelMessageWithSource,
import {
InteractionResponseType,
Routes,
type APICommandAutocompleteInteractionResponseCallbackData,
type APIInteractionResponseCallbackData,
type APIInteractionResponseDeferredChannelMessageWithSource,
type APIModalInteractionResponseCallbackData,
type APIPremiumRequiredInteractionResponse,
type RESTGetAPIWebhookWithTokenMessageResult,
type Snowflake,
} from 'discord-api-types/v10';
import type { WebhooksAPI } from './webhook.js';

Expand Down Expand Up @@ -248,4 +250,26 @@ export class InteractionsAPI {
signal,
});
}

/**
* Sends a premium required response to an interaction
*
* @see {@link https://discord.com/developers/docs/interactions/receiving-and-responding#create-interaction-response}
* @param interactionId - The id of the interaction
* @param interactionToken - The token of the interaction
* @param options - The options for sending the premium required response
*/
public async sendPremiumRequired(
interactionId: Snowflake,
interactionToken: string,
{ signal }: Pick<RequestData, 'signal'> = {},
) {
await this.rest.post(Routes.interactionCallback(interactionId, interactionToken), {
auth: false,
body: {
type: InteractionResponseType.PremiumRequired,
} satisfies APIPremiumRequiredInteractionResponse,
signal,
});
}
}
80 changes: 80 additions & 0 deletions packages/core/src/api/monetization.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/* eslint-disable jsdoc/check-param-names */

import { makeURLSearchParams, type RequestData, type REST } from '@discordjs/rest';
import {
Routes,
type RESTGetAPIEntitlementsQuery,
type RESTGetAPIEntitlementsResult,
type RESTGetAPISKUsResult,
type RESTPostAPIEntitlementBody,
type RESTPostAPIEntitlementResult,
type Snowflake,
} from 'discord-api-types/v10';

export class MonetizationAPI {
public constructor(private readonly rest: REST) {}

/**
* Fetches the SKUs for an application.
*
* @see {@link https://discord.com/developers/docs/monetization/skus#list-skus}
* @param options - The options for fetching the SKUs.
*/
public async getSKUs(applicationId: Snowflake, { signal }: Pick<RequestData, 'signal'> = {}) {
return this.rest.get(Routes.skus(applicationId), { signal }) as Promise<RESTGetAPISKUsResult>;
}

/**
* Fetches the entitlements for an application.
*
* @see {@link https://discord.com/developers/docs/monetization/entitlements#list-entitlements}
* @param applicationId - The application id to fetch entitlements for
* @param query - The query options for fetching entitlements
* @param options - The options for fetching entitlements
*/
public async getEntitlements(
applicationId: Snowflake,
query: RESTGetAPIEntitlementsQuery,
{ signal }: Pick<RequestData, 'signal'> = {},
) {
return this.rest.get(Routes.entitlements(applicationId), {
signal,
query: makeURLSearchParams(query),
}) as Promise<RESTGetAPIEntitlementsResult>;
}

/**
* Creates a test entitlement for an application's SKU.
*
* @see {@link https://discord.com/developers/docs/monetization/entitlements#create-test-entitlement}
* @param applicationId - The application id to create the entitlement for
* @param body - The data for creating the entitlement
* @param options - The options for creating the entitlement
*/
public async createTestEntitlement(
applicationId: Snowflake,
body: RESTPostAPIEntitlementBody,
{ signal }: Pick<RequestData, 'signal'> = {},
) {
return this.rest.post(Routes.entitlements(applicationId), {
body,
signal,
}) as Promise<RESTPostAPIEntitlementResult>;
}

/**
* Deletes a test entitlement for an application's SKU.
*
* @see {@link https://discord.com/developers/docs/monetization/entitlements#delete-test-entitlement}
* @param applicationId - The application id to delete the entitlement for
* @param entitlementId - The entitlement id to delete
* @param options - The options for deleting the entitlement
*/
public async deleteTestEntitlement(
applicationId: Snowflake,
entitlementId: Snowflake,
{ signal }: Pick<RequestData, 'signal'> = {},
) {
await this.rest.delete(Routes.entitlement(applicationId, entitlementId), { signal });
}
}
9 changes: 7 additions & 2 deletions packages/core/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@ import {
type GatewayChannelDeleteDispatchData,
type GatewayChannelPinsUpdateDispatchData,
type GatewayChannelUpdateDispatchData,
type GatewayEntitlementCreateDispatchData,
type GatewayEntitlementDeleteDispatchData,
type GatewayEntitlementUpdateDispatchData,
type GatewayGuildAuditLogEntryCreateDispatchData,
type GatewayGuildBanAddDispatchData,
type GatewayGuildBanRemoveDispatchData,
Expand Down Expand Up @@ -103,6 +106,9 @@ export interface MappedEvents {
[GatewayDispatchEvents.ChannelDelete]: [WithIntrinsicProps<GatewayChannelDeleteDispatchData>];
[GatewayDispatchEvents.ChannelPinsUpdate]: [WithIntrinsicProps<GatewayChannelPinsUpdateDispatchData>];
[GatewayDispatchEvents.ChannelUpdate]: [WithIntrinsicProps<GatewayChannelUpdateDispatchData>];
[GatewayDispatchEvents.EntitlementCreate]: [WithIntrinsicProps<GatewayEntitlementCreateDispatchData>];
[GatewayDispatchEvents.EntitlementDelete]: [WithIntrinsicProps<GatewayEntitlementDeleteDispatchData>];
[GatewayDispatchEvents.EntitlementUpdate]: [WithIntrinsicProps<GatewayEntitlementUpdateDispatchData>];
[GatewayDispatchEvents.GuildAuditLogEntryCreate]: [WithIntrinsicProps<GatewayGuildAuditLogEntryCreateDispatchData>];
[GatewayDispatchEvents.GuildBanAdd]: [WithIntrinsicProps<GatewayGuildBanAddDispatchData>];
[GatewayDispatchEvents.GuildBanRemove]: [WithIntrinsicProps<GatewayGuildBanRemoveDispatchData>];
Expand Down Expand Up @@ -192,9 +198,8 @@ export class Client extends AsyncEventEmitter<MappedEvents> {

this.gateway.on(WebSocketShardEvents.Dispatch, ({ data: dispatch, shardId }) => {
this.emit(
// TODO: move this expect-error down to the next line once entitlements get merged, so missing dispatch types result in errors
// @ts-expect-error event props can't be resolved properly, but they are correct
dispatch.t,
// @ts-expect-error event props can't be resolved properly, but they are correct
this.wrapIntrinsicProps(dispatch.d, shardId),
);
});
Expand Down
3 changes: 3 additions & 0 deletions packages/discord.js/src/client/actions/ActionsManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,9 @@ class ActionsManager {
this.register(require('./ChannelCreate'));
this.register(require('./ChannelDelete'));
this.register(require('./ChannelUpdate'));
this.register(require('./EntitlementCreate'));
this.register(require('./EntitlementDelete'));
this.register(require('./EntitlementUpdate'));
this.register(require('./GuildAuditLogEntryCreate'));
this.register(require('./GuildBanAdd'));
this.register(require('./GuildBanRemove'));
Expand Down
23 changes: 23 additions & 0 deletions packages/discord.js/src/client/actions/EntitlementCreate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';

const Action = require('./Action');
const Events = require('../../util/Events');

class EntitlementCreateAction extends Action {
handle(data) {
const client = this.client;

const entitlement = client.application.entitlements._add(data);

/**
* Emitted whenever an entitlement is created.
* @event Client#entitlementCreate
* @param {Entitlement} entitlement The entitlement that was created
*/
client.emit(Events.EntitlementCreate, entitlement);

return {};
}
}

module.exports = EntitlementCreateAction;
27 changes: 27 additions & 0 deletions packages/discord.js/src/client/actions/EntitlementDelete.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
'use strict';

const Action = require('./Action');
const Events = require('../../util/Events');

class EntitlementDeleteAction extends Action {
handle(data) {
const client = this.client;

const entitlement = client.application.entitlements._add(data, false);

client.application.entitlements.cache.delete(entitlement.id);

/**
* Emitted whenever an entitlement is deleted.
* <warn>Entitlements are not deleted when they expire.
* This is only triggered when Discord issues a refund or deletes the entitlement manually.</warn>
* @event Client#entitlementDelete
* @param {Entitlement} entitlement The entitlement that was deleted
*/
client.emit(Events.EntitlementDelete, entitlement);

return {};
}
}

module.exports = EntitlementDeleteAction;
25 changes: 25 additions & 0 deletions packages/discord.js/src/client/actions/EntitlementUpdate.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
'use strict';

const Action = require('./Action');
const Events = require('../../util/Events');

class EntitlementUpdateAction extends Action {
handle(data) {
const client = this.client;

const oldEntitlement = client.application.entitlements.cache.get(data.id)?._clone() ?? null;
const newEntitlement = client.application.entitlements._add(data);

/**
* Emitted whenever an entitlement is updated - i.e. when a user's subscription renews.
* @event Client#entitlementUpdate
* @param {?Entitlement} oldEntitlement The entitlement before the update
* @param {Entitlement} newEntitlement The entitlement after the update
*/
client.emit(Events.EntitlementUpdate, oldEntitlement, newEntitlement);

return {};
}
}

module.exports = EntitlementUpdateAction;
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = (client, packet) => {
client.actions.EntitlementCreate.handle(packet.d);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = (client, packet) => {
client.actions.EntitlementDelete.handle(packet.d);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
'use strict';

module.exports = (client, packet) => {
client.actions.EntitlementUpdate.handle(packet.d);
};
3 changes: 3 additions & 0 deletions packages/discord.js/src/client/websocket/handlers/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ const handlers = Object.fromEntries([
['CHANNEL_DELETE', require('./CHANNEL_DELETE')],
['CHANNEL_PINS_UPDATE', require('./CHANNEL_PINS_UPDATE')],
['CHANNEL_UPDATE', require('./CHANNEL_UPDATE')],
['ENTITLEMENT_CREATE', require('./ENTITLEMENT_CREATE')],
['ENTITLEMENT_DELETE', require('./ENTITLEMENT_DELETE')],
['ENTITLEMENT_UPDATE', require('./ENTITLEMENT_UPDATE')],
['GUILD_AUDIT_LOG_ENTRY_CREATE', require('./GUILD_AUDIT_LOG_ENTRY_CREATE')],
['GUILD_BAN_ADD', require('./GUILD_BAN_ADD')],
['GUILD_BAN_REMOVE', require('./GUILD_BAN_REMOVE')],
Expand Down
4 changes: 4 additions & 0 deletions packages/discord.js/src/errors/ErrorCodes.js
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,8 @@
* @property {'GuildForumMessageRequired'} GuildForumMessageRequired
* @property {'SweepFilterReturn'} SweepFilterReturn
* @property {'EntitlementCreateInvalidOwner'} EntitlementCreateInvalidOwner
*/

const keys = [
Expand Down Expand Up @@ -323,6 +325,8 @@ const keys = [
'SweepFilterReturn',

'GuildForumMessageRequired',

'EntitlementCreateInvalidOwner',
];

// JSDoc for IntelliSense purposes
Expand Down
3 changes: 3 additions & 0 deletions packages/discord.js/src/errors/Messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ const Messages = {
[DjsErrorCodes.SweepFilterReturn]: 'The return value of the sweepFilter function was not false or a Function',

[DjsErrorCodes.GuildForumMessageRequired]: 'You must provide a message to create a guild forum thread',

[DjsErrorCodes.EntitlementCreateInvalidOwner]:
'You must provide either a guild or a user to create an entitlement, but not both',
};

module.exports = Messages;
4 changes: 4 additions & 0 deletions packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ exports.Partials = require('./util/Partials');
exports.PermissionsBitField = require('./util/PermissionsBitField');
exports.RoleFlagsBitField = require('./util/RoleFlagsBitField');
exports.ShardEvents = require('./util/ShardEvents');
exports.SKUFlagsBitField = require('./util/SKUFlagsBitField').SKUFlagsBitField;
exports.Status = require('./util/Status');
exports.SnowflakeUtil = require('@sapphire/snowflake').DiscordSnowflake;
exports.Sweepers = require('./util/Sweepers');
Expand All @@ -58,6 +59,7 @@ exports.ChannelManager = require('./managers/ChannelManager');
exports.ClientVoiceManager = require('./client/voice/ClientVoiceManager');
exports.DataManager = require('./managers/DataManager');
exports.DMMessageManager = require('./managers/DMMessageManager');
exports.EntitlementManager = require('./managers/EntitlementManager').EntitlementManager;
exports.GuildApplicationCommandManager = require('./managers/GuildApplicationCommandManager');
exports.GuildBanManager = require('./managers/GuildBanManager');
exports.GuildChannelManager = require('./managers/GuildChannelManager');
Expand Down Expand Up @@ -121,6 +123,7 @@ exports.DMChannel = require('./structures/DMChannel');
exports.Embed = require('./structures/Embed');
exports.EmbedBuilder = require('./structures/EmbedBuilder');
exports.Emoji = require('./structures/Emoji').Emoji;
exports.Entitlement = require('./structures/Entitlement').Entitlement;
exports.ForumChannel = require('./structures/ForumChannel');
exports.Guild = require('./structures/Guild').Guild;
exports.GuildAuditLogs = require('./structures/GuildAuditLogs');
Expand Down Expand Up @@ -188,6 +191,7 @@ exports.RoleSelectMenuInteraction = require('./structures/RoleSelectMenuInteract
exports.StringSelectMenuInteraction = require('./structures/StringSelectMenuInteraction');
exports.UserSelectMenuInteraction = require('./structures/UserSelectMenuInteraction');
exports.SelectMenuOptionBuilder = require('./structures/SelectMenuOptionBuilder');
exports.SKU = require('./structures/SKU').SKU;
exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder');
exports.StageChannel = require('./structures/StageChannel');
exports.StageInstance = require('./structures/StageInstance').StageInstance;
Expand Down
Loading

0 comments on commit c4fcee3

Please sign in to comment.