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: add subscriptions #10541

Merged
merged 12 commits into from
Nov 28, 2024
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 @@ -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')],
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 @@ -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');
Expand Down Expand Up @@ -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');
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 @@ -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
Expand Down Expand Up @@ -147,6 +150,9 @@ module.exports = {
StageInstanceCreate: 'stageInstanceCreate',
StageInstanceDelete: 'stageInstanceDelete',
StageInstanceUpdate: 'stageInstanceUpdate',
SubscriptionCreate: 'subscriptionCreate',
SubscriptionUpdate: 'subscriptionUpdate',
SubscriptionDelete: 'subscriptionDelete',
ThreadCreate: 'threadCreate',
ThreadDelete: 'threadDelete',
ThreadListSync: 'threadListSync',
Expand Down
Loading