Skip to content

Commit

Permalink
feat: add subscriptions (#10541)
Browse files Browse the repository at this point in the history
* 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 ba472bd.

* 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>
  • Loading branch information
3 people committed Dec 2, 2024
1 parent 388783d commit 4cca33d
Show file tree
Hide file tree
Showing 11 changed files with 301 additions and 6 deletions.
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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);
};
Original file line number Diff line number Diff line change
@@ -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);
};
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 @@ -53,6 +53,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')],
Expand Down
2 changes: 2 additions & 0 deletions packages/discord.js/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,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');
Expand Down Expand Up @@ -202,6 +203,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');
Expand Down
81 changes: 81 additions & 0 deletions packages/discord.js/src/managers/SubscriptionManager.js
Original file line number Diff line number Diff line change
@@ -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;
7 changes: 7 additions & 0 deletions packages/discord.js/src/structures/ClientApplication.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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) {
Expand Down
8 changes: 2 additions & 6 deletions packages/discord.js/src/structures/Entitlement.js
Original file line number Diff line number Diff line change
Expand Up @@ -73,21 +73,19 @@ 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;
}

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;
}
Expand All @@ -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() {
Expand All @@ -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() {
Expand Down
109 changes: 109 additions & 0 deletions packages/discord.js/src/structures/Subscription.js
Original file line number Diff line number Diff line change
@@ -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;
6 changes: 6 additions & 0 deletions packages/discord.js/src/util/Events.js
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,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
Expand Down Expand Up @@ -158,6 +161,9 @@ module.exports = {
StageInstanceCreate: 'stageInstanceCreate',
StageInstanceDelete: 'stageInstanceDelete',
StageInstanceUpdate: 'stageInstanceUpdate',
SubscriptionCreate: 'subscriptionCreate',
SubscriptionUpdate: 'subscriptionUpdate',
SubscriptionDelete: 'subscriptionDelete',
ThreadCreate: 'threadCreate',
ThreadDelete: 'threadDelete',
ThreadListSync: 'threadListSync',
Expand Down
Loading

0 comments on commit 4cca33d

Please sign in to comment.