diff options
Diffstat (limited to 'node_modules/discord.js/src/util/Sweepers.js')
-rw-r--r-- | node_modules/discord.js/src/util/Sweepers.js | 467 |
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; |