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

feat: premium application subscriptions #9907

Merged
merged 5 commits into from
Dec 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
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