feat: add subscriptions (#10541)

* feat: add subscriptions

* types: fix fetch options types

* fix: correct properties in patch method

* chore: requested changes

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

* fix: correct export syntax

* chore(Entitlement): mark `ends_at` as nullable`

* types(FetchSubscriptionOptions): add missing `cache` option

* Revert "types(FetchSubscriptionOptions): add missing `cache` option"

This reverts commit ba472bdc599e1860754e59fce4806610f06ac682.

* chore(Entitlement): mark `startsTimestamp` as nullable

* fix: requested changes

* docs(SubscriptionManager): correct return type

---------

Co-authored-by: Jiralite <33201955+Jiralite@users.noreply.github.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
This commit is contained in:
Danial Raza
2024-11-28 09:18:44 +01:00
committed by GitHub
parent 9010b121f4
commit 108943a397
11 changed files with 301 additions and 6 deletions

View File

@@ -0,0 +1,14 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const subscription = client.application.subscriptions._add(data);
/**
* Emitted whenever a subscription is created.
* @event Client#subscriptionCreate
* @param {Subscription} subscription The subscription that was created
*/
client.emit(Events.SubscriptionCreate, subscription);
};

View File

@@ -0,0 +1,16 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const subscription = client.application.subscriptions._add(data, false);
client.application.subscriptions.cache.delete(subscription.id);
/**
* Emitted whenever a subscription is deleted.
* @event Client#subscriptionDelete
* @param {Subscription} subscription The subscription that was deleted
*/
client.emit(Events.SubscriptionDelete, subscription);
};

View File

@@ -0,0 +1,16 @@
'use strict';
const Events = require('../../../util/Events');
module.exports = (client, { d: data }) => {
const oldSubscription = client.application.subscriptions.cache.get(data.id)?._clone() ?? null;
const newSubscription = client.application.subscriptions._add(data);
/**
* Emitted whenever a subscription is updated - i.e. when a user's subscription renews.
* @event Client#subscriptionUpdate
* @param {?Subscription} oldSubscription The subscription before the update
* @param {Subscription} newSubscription The subscription after the update
*/
client.emit(Events.SubscriptionUpdate, oldSubscription, newSubscription);
};

View File

@@ -52,6 +52,9 @@ const handlers = Object.fromEntries([
['STAGE_INSTANCE_CREATE', require('./STAGE_INSTANCE_CREATE')],
['STAGE_INSTANCE_DELETE', require('./STAGE_INSTANCE_DELETE')],
['STAGE_INSTANCE_UPDATE', require('./STAGE_INSTANCE_UPDATE')],
['SUBSCRIPTION_CREATE', require('./SUBSCRIPTION_CREATE')],
['SUBSCRIPTION_DELETE', require('./SUBSCRIPTION_DELETE')],
['SUBSCRIPTION_UPDATE', require('./SUBSCRIPTION_UPDATE')],
['THREAD_CREATE', require('./THREAD_CREATE')],
['THREAD_DELETE', require('./THREAD_DELETE')],
['THREAD_LIST_SYNC', require('./THREAD_LIST_SYNC')],

View File

@@ -83,6 +83,7 @@ exports.ReactionManager = require('./managers/ReactionManager');
exports.ReactionUserManager = require('./managers/ReactionUserManager');
exports.RoleManager = require('./managers/RoleManager');
exports.StageInstanceManager = require('./managers/StageInstanceManager');
exports.SubscriptionManager = require('./managers/SubscriptionManager').SubscriptionManager;
exports.ThreadManager = require('./managers/ThreadManager');
exports.ThreadMemberManager = require('./managers/ThreadMemberManager');
exports.UserManager = require('./managers/UserManager');
@@ -193,6 +194,7 @@ exports.SKU = require('./structures/SKU').SKU;
exports.StringSelectMenuOptionBuilder = require('./structures/StringSelectMenuOptionBuilder');
exports.StageChannel = require('./structures/StageChannel');
exports.StageInstance = require('./structures/StageInstance').StageInstance;
exports.Subscription = require('./structures/Subscription').Subscription;
exports.Sticker = require('./structures/Sticker').Sticker;
exports.StickerPack = require('./structures/StickerPack');
exports.Team = require('./structures/Team');

View File

@@ -0,0 +1,81 @@
'use strict';
const { Collection } = require('@discordjs/collection');
const { makeURLSearchParams } = require('@discordjs/rest');
const { Routes } = require('discord-api-types/v10');
const CachedManager = require('./CachedManager');
const { DiscordjsTypeError, ErrorCodes } = require('../errors/index');
const { Subscription } = require('../structures/Subscription');
const { resolveSKUId } = require('../util/Util');
/**
* Manages API methods for subscriptions and stores their cache.
* @extends {CachedManager}
*/
class SubscriptionManager extends CachedManager {
constructor(client, iterable) {
super(client, Subscription, iterable);
}
/**
* The cache of this manager
* @type {Collection<Snowflake, Subscription>}
* @name SubscriptionManager#cache
*/
/**
* Options used to fetch a subscription
* @typedef {BaseFetchOptions} FetchSubscriptionOptions
* @property {SKUResolvable} sku The SKU to fetch the subscription for
* @property {Snowflake} subscriptionId The id of the subscription to fetch
*/
/**
* Options used to fetch subscriptions
* @typedef {Object} FetchSubscriptionsOptions
* @property {Snowflake} [after] Consider only subscriptions after this subscription id
* @property {Snowflake} [before] Consider only subscriptions before this subscription id
* @property {number} [limit] The maximum number of subscriptions to fetch
* @property {SKUResolvable} sku The SKU to fetch subscriptions for
* @property {UserResolvable} user The user to fetch entitlements for
* <warn>If both `before` and `after` are provided, only `before` is respected</warn>
*/
/**
* Fetches subscriptions for this application
* @param {FetchSubscriptionOptions|FetchSubscriptionsOptions} [options={}] Options for fetching the subscriptions
* @returns {Promise<Subscription|Collection<Snowflake, Subscription>>}
*/
async fetch(options = {}) {
if (typeof options !== 'object') throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'options', 'object', true);
const { after, before, cache, limit, sku, subscriptionId, user } = options;
const skuId = resolveSKUId(sku);
if (!skuId) throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'sku', 'SKUResolvable');
if (subscriptionId) {
const subscription = await this.client.rest.get(Routes.skuSubscription(skuId, subscriptionId));
return this._add(subscription, cache);
}
const query = makeURLSearchParams({
limit,
user_id: this.client.users.resolveId(user) ?? undefined,
sku_id: skuId,
before,
after,
});
const subscriptions = await this.client.rest.get(Routes.skuSubscriptions(skuId), { query });
return subscriptions.reduce(
(coll, subscription) => coll.set(subscription.id, this._add(subscription, cache)),
new Collection(),
);
}
}
exports.SubscriptionManager = SubscriptionManager;

View File

@@ -9,6 +9,7 @@ const Application = require('./interfaces/Application');
const ApplicationCommandManager = require('../managers/ApplicationCommandManager');
const ApplicationEmojiManager = require('../managers/ApplicationEmojiManager');
const { EntitlementManager } = require('../managers/EntitlementManager');
const { SubscriptionManager } = require('../managers/SubscriptionManager');
const ApplicationFlagsBitField = require('../util/ApplicationFlagsBitField');
const { resolveImage } = require('../util/DataResolver');
const PermissionsBitField = require('../util/PermissionsBitField');
@@ -44,6 +45,12 @@ class ClientApplication extends Application {
* @type {EntitlementManager}
*/
this.entitlements = new EntitlementManager(this.client);
/**
* The subscription manager for this application
* @type {SubscriptionManager}
*/
this.subscriptions = new SubscriptionManager(this.client);
}
_patch(data) {

View File

@@ -73,10 +73,9 @@ class Entitlement extends Base {
if ('starts_at' in data) {
/**
* The timestamp at which this entitlement is valid
* <info>This is only `null` for test entitlements</info>
* @type {?number}
*/
this.startsTimestamp = Date.parse(data.starts_at);
this.startsTimestamp = data.starts_at ? Date.parse(data.starts_at) : null;
} else {
this.startsTimestamp ??= null;
}
@@ -84,10 +83,9 @@ class Entitlement extends Base {
if ('ends_at' in data) {
/**
* The timestamp at which this entitlement is no longer valid
* <info>This is only `null` for test entitlements</info>
* @type {?number}
*/
this.endsTimestamp = Date.parse(data.ends_at);
this.endsTimestamp = data.ends_at ? Date.parse(data.ends_at) : null;
} else {
this.endsTimestamp ??= null;
}
@@ -114,7 +112,6 @@ class Entitlement extends Base {
/**
* The start date at which this entitlement is valid
* <info>This is only `null` for test entitlements</info>
* @type {?Date}
*/
get startsAt() {
@@ -123,7 +120,6 @@ class Entitlement extends Base {
/**
* The end date at which this entitlement is no longer valid
* <info>This is only `null` for test entitlements</info>
* @type {?Date}
*/
get endsAt() {

View File

@@ -0,0 +1,109 @@
'use strict';
const Base = require('./Base');
/**
* Represents a Subscription
* @extends {Base}
*/
class Subscription extends Base {
constructor(client, data) {
super(client);
/**
* The id of the subscription
* @type {Snowflake}
*/
this.id = data.id;
/**
* The id of the user who subscribed
* @type {Snowflake}
*/
this.userId = data.user_id;
this._patch(data);
}
_patch(data) {
/**
* The SKU ids subscribed to
* @type {Snowflake[]}
*/
this.skuIds = data.sku_ids;
/**
* The entitlement ids granted for this subscription
* @type {Snowflake[]}
*/
this.entitlementIds = data.entitlement_ids;
/**
* The timestamp the current subscription period will start at
* @type {number}
*/
this.currentPeriodStartTimestamp = Date.parse(data.current_period_start);
/**
* The timestamp the current subscription period will end at
* @type {number}
*/
this.currentPeriodEndTimestamp = Date.parse(data.current_period_end);
/**
* The current status of the subscription
* @type {SubscriptionStatus}
*/
this.status = data.status;
if ('canceled_at' in data) {
/**
* The timestamp of when the subscription was canceled
* @type {?number}
*/
this.canceledTimestamp = data.canceled_at ? Date.parse(data.canceled_at) : null;
} else {
this.canceledTimestamp ??= null;
}
if ('country' in data) {
/**
* ISO 3166-1 alpha-2 country code of the payment source used to purchase the subscription.
* Missing unless queried with a private OAuth scope.
* @type {?string}
*/
this.country = data.country;
} else {
this.country ??= null;
}
}
/**
* The time the subscription was canceled
* @type {?Date}
* @readonly
*/
get canceledAt() {
return this.canceledTimestamp && new Date(this.canceledTimestamp);
}
/**
* The time the current subscription period will start at
* @type {Date}
* @readonly
*/
get currentPeriodStartAt() {
return new Date(this.currentPeriodStartTimestamp);
}
/**
* The time the current subscription period will end at
* @type {Date}
* @readonly
*/
get currentPeriodEndAt() {
return new Date(this.currentPeriodEndTimestamp);
}
}
exports.Subscription = Subscription;

View File

@@ -64,6 +64,9 @@
* @property {string} StageInstanceCreate stageInstanceCreate
* @property {string} StageInstanceDelete stageInstanceDelete
* @property {string} StageInstanceUpdate stageInstanceUpdate
* @property {string} SubscriptionCreate subscriptionCreate
* @property {string} SubscriptionUpdate subscriptionUpdate
* @property {string} SubscriptionDelete subscriptionDelete
* @property {string} ThreadCreate threadCreate
* @property {string} ThreadDelete threadDelete
* @property {string} ThreadListSync threadListSync
@@ -147,6 +150,9 @@ module.exports = {
StageInstanceCreate: 'stageInstanceCreate',
StageInstanceDelete: 'stageInstanceDelete',
StageInstanceUpdate: 'stageInstanceUpdate',
SubscriptionCreate: 'subscriptionCreate',
SubscriptionUpdate: 'subscriptionUpdate',
SubscriptionDelete: 'subscriptionDelete',
ThreadCreate: 'threadCreate',
ThreadDelete: 'threadDelete',
ThreadListSync: 'threadListSync',

View File

@@ -164,6 +164,8 @@ import {
GuildScheduledEventRecurrenceRuleWeekday,
GuildScheduledEventRecurrenceRuleMonth,
GuildScheduledEventRecurrenceRuleFrequency,
APISubscription,
SubscriptionStatus,
GatewaySendPayload,
GatewayDispatchPayload,
VoiceChannelEffectSendAnimationType,
@@ -1053,6 +1055,7 @@ export class ClientApplication extends Application {
public commands: ApplicationCommandManager;
public emojis: ApplicationEmojiManager;
public entitlements: EntitlementManager;
public subscriptions: SubscriptionManager;
public guildId: Snowflake | null;
public get guild(): Guild | null;
public cover: string | null;
@@ -3078,6 +3081,22 @@ export class SKUFlagsBitField extends BitField<SKUFlagsString> {
public static resolve(bit?: BitFieldResolvable<SKUFlagsString, number>): number;
}
export class Subscription extends Base {
private constructor(client: Client<true>, data: APISubscription);
public id: Snowflake;
public userId: Snowflake;
public skuIds: Snowflake[];
public entitlementIds: Snowflake[];
public currentPeriodStartTimestamp: number;
public currentPeriodEndTimestamp: number;
public status: SubscriptionStatus;
public canceledTimestamp: number | null;
public country: string | null;
public get canceledAt(): Date | null;
public get currentPeriodStartAt(): Date;
public get currentPeriodEndAt(): Date;
}
export class StageChannel extends BaseGuildVoiceChannel {
public get stageInstance(): StageInstance | null;
public topic: string | null;
@@ -4069,6 +4088,7 @@ export class ChannelManager extends CachedManager<Snowflake, Channel, ChannelRes
export type EntitlementResolvable = Snowflake | Entitlement;
export type SKUResolvable = Snowflake | SKU;
export type SubscriptionResolvable = Snowflake | Subscription;
export interface GuildEntitlementCreateOptions {
sku: SKUResolvable;
@@ -4099,6 +4119,25 @@ export class EntitlementManager extends CachedManager<Snowflake, Entitlement, En
public consume(entitlementId: Snowflake): Promise<void>;
}
export interface FetchSubscriptionOptions extends BaseFetchOptions {
sku: SKUResolvable;
subscriptionId: Snowflake;
}
export interface FetchSubscriptionsOptions {
after?: Snowflake;
before?: Snowflake;
limit?: number;
sku: SKUResolvable;
user: UserResolvable;
}
export class SubscriptionManager extends CachedManager<Snowflake, Subscription, SubscriptionResolvable> {
private constructor(client: Client<true>, iterable?: Iterable<APISubscription>);
public fetch(options: FetchSubscriptionOptions): Promise<Subscription>;
public fetch(options: FetchSubscriptionsOptions): Promise<Collection<Snowflake, Subscription>>;
}
export interface FetchGuildApplicationCommandFetchOptions extends Omit<FetchApplicationCommandOptions, 'guildId'> {}
export class GuildApplicationCommandManager extends ApplicationCommandManager<ApplicationCommand, {}, Guild> {
@@ -5193,6 +5232,9 @@ export interface ClientEvents {
stickerCreate: [sticker: Sticker];
stickerDelete: [sticker: Sticker];
stickerUpdate: [oldSticker: Sticker, newSticker: Sticker];
subscriptionCreate: [subscription: Subscription];
subscriptionDelete: [subscription: Subscription];
subscriptionUpdate: [oldSubscription: Subscription | null, newSubscription: Subscription];
guildScheduledEventCreate: [guildScheduledEvent: GuildScheduledEvent];
guildScheduledEventUpdate: [
oldGuildScheduledEvent: GuildScheduledEvent | PartialGuildScheduledEvent | null,
@@ -5392,6 +5434,9 @@ export enum Events {
StageInstanceCreate = 'stageInstanceCreate',
StageInstanceUpdate = 'stageInstanceUpdate',
StageInstanceDelete = 'stageInstanceDelete',
SubscriptionCreate = 'subscriptionCreate',
SubscriptionUpdate = 'subscriptionUpdate',
SubscriptionDelete = 'subscriptionDelete',
GuildStickerCreate = 'stickerCreate',
GuildStickerDelete = 'stickerDelete',
GuildStickerUpdate = 'stickerUpdate',