summaryrefslogtreecommitdiff
path: root/node_modules/discord.js/src/util/Sweepers.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/discord.js/src/util/Sweepers.js')
-rw-r--r--node_modules/discord.js/src/util/Sweepers.js467
1 files changed, 467 insertions, 0 deletions
diff --git a/node_modules/discord.js/src/util/Sweepers.js b/node_modules/discord.js/src/util/Sweepers.js
new file mode 100644
index 0000000..6eb2dc6
--- /dev/null
+++ b/node_modules/discord.js/src/util/Sweepers.js
@@ -0,0 +1,467 @@
+'use strict';
+
+const { setInterval, clearInterval } = require('node:timers');
+const { ThreadChannelTypes, SweeperKeys } = require('./Constants');
+const Events = require('./Events');
+const { DiscordjsTypeError, ErrorCodes } = require('../errors');
+
+/**
+ * @typedef {Function} GlobalSweepFilter
+ * @returns {Function|null} Return `null` to skip sweeping, otherwise a function passed to `sweep()`,
+ * See {@link [Collection#sweep](https://discord.js.org/docs/packages/collection/stable/Collection:Class#sweep)}
+ * for the definition of this function.
+ */
+
+/**
+ * A container for all cache sweeping intervals and their associated sweep methods.
+ */
+class Sweepers {
+ constructor(client, options) {
+ /**
+ * The client that instantiated this
+ * @type {Client}
+ * @readonly
+ */
+ Object.defineProperty(this, 'client', { value: client });
+
+ /**
+ * The options the sweepers were instantiated with
+ * @type {SweeperOptions}
+ */
+ this.options = options;
+
+ /**
+ * A record of interval timeout that is used to sweep the indicated items, or null if not being swept
+ * @type {Object<SweeperKey, ?Timeout>}
+ */
+ this.intervals = Object.fromEntries(SweeperKeys.map(key => [key, null]));
+
+ for (const key of SweeperKeys) {
+ if (!(key in options)) continue;
+
+ this._validateProperties(key);
+
+ const clonedOptions = { ...this.options[key] };
+
+ // Handle cases that have a "lifetime"
+ if (!('filter' in clonedOptions)) {
+ switch (key) {
+ case 'invites':
+ clonedOptions.filter = this.constructor.expiredInviteSweepFilter(clonedOptions.lifetime);
+ break;
+ case 'messages':
+ clonedOptions.filter = this.constructor.outdatedMessageSweepFilter(clonedOptions.lifetime);
+ break;
+ case 'threads':
+ clonedOptions.filter = this.constructor.archivedThreadSweepFilter(clonedOptions.lifetime);
+ }
+ }
+
+ this._initInterval(key, `sweep${key[0].toUpperCase()}${key.slice(1)}`, clonedOptions);
+ }
+ }
+
+ /**
+ * Sweeps all guild and global application commands and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which commands will be removed from the caches.
+ * @returns {number} Amount of commands that were removed from the caches
+ */
+ sweepApplicationCommands(filter) {
+ const { guilds, items: guildCommands } = this._sweepGuildDirectProp('commands', filter, { emit: false });
+
+ const globalCommands = this.client.application?.commands.cache.sweep(filter) ?? 0;
+
+ this.client.emit(
+ Events.CacheSweep,
+ `Swept ${globalCommands} global application commands and ${guildCommands} guild commands in ${guilds} guilds.`,
+ );
+ return guildCommands + globalCommands;
+ }
+
+ /**
+ * Sweeps all auto moderation rules and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine
+ * which auto moderation rules will be removed from the caches
+ * @returns {number} Amount of auto moderation rules that were removed from the caches
+ */
+ sweepAutoModerationRules(filter) {
+ return this._sweepGuildDirectProp('autoModerationRules', filter).items;
+ }
+
+ /**
+ * Sweeps all guild bans and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which bans will be removed from the caches.
+ * @returns {number} Amount of bans that were removed from the caches
+ */
+ sweepBans(filter) {
+ return this._sweepGuildDirectProp('bans', filter).items;
+ }
+
+ /**
+ * Sweeps all guild emojis and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which emojis will be removed from the caches.
+ * @returns {number} Amount of emojis that were removed from the caches
+ */
+ sweepEmojis(filter) {
+ return this._sweepGuildDirectProp('emojis', filter).items;
+ }
+
+ /**
+ * Sweeps all guild invites and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which invites will be removed from the caches.
+ * @returns {number} Amount of invites that were removed from the caches
+ */
+ sweepInvites(filter) {
+ return this._sweepGuildDirectProp('invites', filter).items;
+ }
+
+ /**
+ * Sweeps all guild members and removes the ones which are indicated by the filter.
+ * <info>It is highly recommended to keep the client guild member cached</info>
+ * @param {Function} filter The function used to determine which guild members will be removed from the caches.
+ * @returns {number} Amount of guild members that were removed from the caches
+ */
+ sweepGuildMembers(filter) {
+ return this._sweepGuildDirectProp('members', filter, { outputName: 'guild members' }).items;
+ }
+
+ /**
+ * Sweeps all text-based channels' messages and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which messages will be removed from the caches.
+ * @returns {number} Amount of messages that were removed from the caches
+ * @example
+ * // Remove all messages older than 1800 seconds from the messages cache
+ * const amount = sweepers.sweepMessages(
+ * Sweepers.filterByLifetime({
+ * lifetime: 1800,
+ * getComparisonTimestamp: m => m.editedTimestamp ?? m.createdTimestamp,
+ * })(),
+ * );
+ * console.log(`Successfully removed ${amount} messages from the cache.`);
+ */
+ sweepMessages(filter) {
+ if (typeof filter !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'filter', 'function');
+ }
+ let channels = 0;
+ let messages = 0;
+
+ for (const channel of this.client.channels.cache.values()) {
+ if (!channel.isTextBased()) continue;
+
+ channels++;
+ messages += channel.messages.cache.sweep(filter);
+ }
+ this.client.emit(Events.CacheSweep, `Swept ${messages} messages in ${channels} text-based channels.`);
+ return messages;
+ }
+
+ /**
+ * Sweeps all presences and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which presences will be removed from the caches.
+ * @returns {number} Amount of presences that were removed from the caches
+ */
+ sweepPresences(filter) {
+ return this._sweepGuildDirectProp('presences', filter).items;
+ }
+
+ /**
+ * Sweeps all message reactions and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which reactions will be removed from the caches.
+ * @returns {number} Amount of reactions that were removed from the caches
+ */
+ sweepReactions(filter) {
+ if (typeof filter !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'filter', 'function');
+ }
+ let channels = 0;
+ let messages = 0;
+ let reactions = 0;
+
+ for (const channel of this.client.channels.cache.values()) {
+ if (!channel.isTextBased()) continue;
+ channels++;
+
+ for (const message of channel.messages.cache.values()) {
+ messages++;
+ reactions += message.reactions.cache.sweep(filter);
+ }
+ }
+ this.client.emit(
+ Events.CacheSweep,
+ `Swept ${reactions} reactions on ${messages} messages in ${channels} text-based channels.`,
+ );
+ return reactions;
+ }
+
+ /**
+ * Sweeps all guild stage instances and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which stage instances will be removed from the caches.
+ * @returns {number} Amount of stage instances that were removed from the caches
+ */
+ sweepStageInstances(filter) {
+ return this._sweepGuildDirectProp('stageInstances', filter, { outputName: 'stage instances' }).items;
+ }
+
+ /**
+ * Sweeps all guild stickers and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which stickers will be removed from the caches.
+ * @returns {number} Amount of stickers that were removed from the caches
+ */
+ sweepStickers(filter) {
+ return this._sweepGuildDirectProp('stickers', filter).items;
+ }
+
+ /**
+ * Sweeps all thread members and removes the ones which are indicated by the filter.
+ * <info>It is highly recommended to keep the client thread member cached</info>
+ * @param {Function} filter The function used to determine which thread members will be removed from the caches.
+ * @returns {number} Amount of thread members that were removed from the caches
+ */
+ sweepThreadMembers(filter) {
+ if (typeof filter !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'filter', 'function');
+ }
+
+ let threads = 0;
+ let members = 0;
+ for (const channel of this.client.channels.cache.values()) {
+ if (!ThreadChannelTypes.includes(channel.type)) continue;
+ threads++;
+ members += channel.members.cache.sweep(filter);
+ }
+ this.client.emit(Events.CacheSweep, `Swept ${members} thread members in ${threads} threads.`);
+ return members;
+ }
+
+ /**
+ * Sweeps all threads and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which threads will be removed from the caches.
+ * @returns {number} filter Amount of threads that were removed from the caches
+ * @example
+ * // Remove all threads archived greater than 1 day ago from all the channel caches
+ * const amount = sweepers.sweepThreads(
+ * Sweepers.filterByLifetime({
+ * getComparisonTimestamp: t => t.archivedTimestamp,
+ * excludeFromSweep: t => !t.archived,
+ * })(),
+ * );
+ * console.log(`Successfully removed ${amount} threads from the cache.`);
+ */
+ sweepThreads(filter) {
+ if (typeof filter !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'filter', 'function');
+ }
+
+ let threads = 0;
+ for (const [key, val] of this.client.channels.cache.entries()) {
+ if (!ThreadChannelTypes.includes(val.type)) continue;
+ if (filter(val, key, this.client.channels.cache)) {
+ threads++;
+ this.client.channels._remove(key);
+ }
+ }
+ this.client.emit(Events.CacheSweep, `Swept ${threads} threads.`);
+ return threads;
+ }
+
+ /**
+ * Sweeps all users and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which users will be removed from the caches.
+ * @returns {number} Amount of users that were removed from the caches
+ */
+ sweepUsers(filter) {
+ if (typeof filter !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'filter', 'function');
+ }
+
+ const users = this.client.users.cache.sweep(filter);
+
+ this.client.emit(Events.CacheSweep, `Swept ${users} users.`);
+
+ return users;
+ }
+
+ /**
+ * Sweeps all guild voice states and removes the ones which are indicated by the filter.
+ * @param {Function} filter The function used to determine which voice states will be removed from the caches.
+ * @returns {number} Amount of voice states that were removed from the caches
+ */
+ sweepVoiceStates(filter) {
+ return this._sweepGuildDirectProp('voiceStates', filter, { outputName: 'voice states' }).items;
+ }
+
+ /**
+ * Cancels all sweeping intervals
+ * @returns {void}
+ */
+ destroy() {
+ for (const key of SweeperKeys) {
+ if (this.intervals[key]) clearInterval(this.intervals[key]);
+ }
+ }
+
+ /**
+ * Options for generating a filter function based on lifetime
+ * @typedef {Object} LifetimeFilterOptions
+ * @property {number} [lifetime=14400] How long, in seconds, an entry should stay in the collection
+ * before it is considered sweepable.
+ * @property {Function} [getComparisonTimestamp=e => e?.createdTimestamp] A function that takes an entry, key,
+ * and the collection and returns a timestamp to compare against in order to determine the lifetime of the entry.
+ * @property {Function} [excludeFromSweep=() => false] A function that takes an entry, key, and the collection
+ * and returns a boolean, `true` when the entry should not be checked for sweepability.
+ */
+
+ /**
+ * Create a sweepFilter function that uses a lifetime to determine sweepability.
+ * @param {LifetimeFilterOptions} [options={}] The options used to generate the filter function
+ * @returns {GlobalSweepFilter}
+ */
+ static filterByLifetime({
+ lifetime = 14400,
+ getComparisonTimestamp = e => e?.createdTimestamp,
+ excludeFromSweep = () => false,
+ } = {}) {
+ if (typeof lifetime !== 'number') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'lifetime', 'number');
+ }
+ if (typeof getComparisonTimestamp !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'getComparisonTimestamp', 'function');
+ }
+ if (typeof excludeFromSweep !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'excludeFromSweep', 'function');
+ }
+ return () => {
+ if (lifetime <= 0) return null;
+ const lifetimeMs = lifetime * 1_000;
+ const now = Date.now();
+ return (entry, key, coll) => {
+ if (excludeFromSweep(entry, key, coll)) {
+ return false;
+ }
+ const comparisonTimestamp = getComparisonTimestamp(entry, key, coll);
+ if (!comparisonTimestamp || typeof comparisonTimestamp !== 'number') return false;
+ return now - comparisonTimestamp > lifetimeMs;
+ };
+ };
+ }
+
+ /**
+ * Creates a sweep filter that sweeps archived threads
+ * @param {number} [lifetime=14400] How long a thread has to be archived to be valid for sweeping
+ * @returns {GlobalSweepFilter}
+ */
+ static archivedThreadSweepFilter(lifetime = 14400) {
+ return this.filterByLifetime({
+ lifetime,
+ getComparisonTimestamp: e => e.archiveTimestamp,
+ excludeFromSweep: e => !e.archived,
+ });
+ }
+
+ /**
+ * Creates a sweep filter that sweeps expired invites
+ * @param {number} [lifetime=14400] How long ago an invite has to have expired to be valid for sweeping
+ * @returns {GlobalSweepFilter}
+ */
+ static expiredInviteSweepFilter(lifetime = 14400) {
+ return this.filterByLifetime({
+ lifetime,
+ getComparisonTimestamp: i => i.expiresTimestamp,
+ });
+ }
+
+ /**
+ * Creates a sweep filter that sweeps outdated messages (edits taken into account)
+ * @param {number} [lifetime=3600] How long ago a message has to have been sent or edited to be valid for sweeping
+ * @returns {GlobalSweepFilter}
+ */
+ static outdatedMessageSweepFilter(lifetime = 3600) {
+ return this.filterByLifetime({
+ lifetime,
+ getComparisonTimestamp: m => m.editedTimestamp ?? m.createdTimestamp,
+ });
+ }
+
+ /**
+ * Configuration options for emitting the cache sweep client event
+ * @typedef {Object} SweepEventOptions
+ * @property {boolean} [emit=true] Whether to emit the client event in this method
+ * @property {string} [outputName] A name to output in the client event if it should differ from the key
+ * @private
+ */
+
+ /**
+ * Sweep a direct sub property of all guilds
+ * @param {string} key The name of the property
+ * @param {Function} filter Filter function passed to sweep
+ * @param {SweepEventOptions} [eventOptions={}] Options for the Client event emitted here
+ * @returns {Object} Object containing the number of guilds swept and the number of items swept
+ * @private
+ */
+ _sweepGuildDirectProp(key, filter, { emit = true, outputName } = {}) {
+ if (typeof filter !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, 'filter', 'function');
+ }
+
+ let guilds = 0;
+ let items = 0;
+
+ for (const guild of this.client.guilds.cache.values()) {
+ const { cache } = guild[key];
+
+ guilds++;
+ items += cache.sweep(filter);
+ }
+
+ if (emit) {
+ this.client.emit(Events.CacheSweep, `Swept ${items} ${outputName ?? key} in ${guilds} guilds.`);
+ }
+
+ return { guilds, items };
+ }
+
+ /**
+ * Validates a set of properties
+ * @param {string} key Key of the options object to check
+ * @private
+ */
+ _validateProperties(key) {
+ const props = this.options[key];
+ if (typeof props !== 'object') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, `sweepers.${key}`, 'object', true);
+ }
+ if (typeof props.interval !== 'number') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, `sweepers.${key}.interval`, 'number');
+ }
+ // Invites, Messages, and Threads can be provided a lifetime parameter, which we use to generate the filter
+ if (['invites', 'messages', 'threads'].includes(key) && !('filter' in props)) {
+ if (typeof props.lifetime !== 'number') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, `sweepers.${key}.lifetime`, 'number');
+ }
+ return;
+ }
+ if (typeof props.filter !== 'function') {
+ throw new DiscordjsTypeError(ErrorCodes.InvalidType, `sweepers.${key}.filter`, 'function');
+ }
+ }
+
+ /**
+ * Initialize an interval for sweeping
+ * @param {string} intervalKey The name of the property that stores the interval for this sweeper
+ * @param {string} sweepKey The name of the function that sweeps the desired caches
+ * @param {Object} opts Validated options for a sweep
+ * @private
+ */
+ _initInterval(intervalKey, sweepKey, opts) {
+ if (opts.interval <= 0 || opts.interval === Infinity) return;
+ this.intervals[intervalKey] = setInterval(() => {
+ const sweepFn = opts.filter();
+ if (sweepFn === null) return;
+ if (typeof sweepFn !== 'function') throw new DiscordjsTypeError(ErrorCodes.SweepFilterReturn);
+ this[sweepKey](sweepFn);
+ }, opts.interval * 1_000).unref();
+ }
+}
+
+module.exports = Sweepers;