discord.js-selfbot-v13/src/util/Sweepers.js
March 7th 294005bac7 feat: automod
#8886 djs
2022-12-20 22:46:50 +07:00

467 lines
17 KiB
JavaScript

'use strict';
const { setInterval } = require('node:timers');
const { Events, ThreadChannelTypes, SweeperKeys } = require('./Constants');
const { TypeError } = require('../errors/DJSError.js');
/**
* @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/collection/main/class/Collection?scrollTo=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.CACHE_SWEEP,
`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 TypeError('INVALID_TYPE', 'filter', 'function');
}
let channels = 0;
let messages = 0;
for (const channel of this.client.channels.cache.values()) {
if (!channel.isText()) continue;
channels++;
messages += channel.messages.cache.sweep(filter);
}
this.client.emit(Events.CACHE_SWEEP, `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 TypeError('INVALID_TYPE', 'filter', 'function');
}
let channels = 0;
let messages = 0;
let reactions = 0;
for (const channel of this.client.channels.cache.values()) {
if (!channel.isText()) continue;
channels++;
for (const message of channel.messages.cache.values()) {
messages++;
reactions += message.reactions.cache.sweep(filter);
}
}
this.client.emit(
Events.CACHE_SWEEP,
`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 TypeError('INVALID_TYPE', '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.CACHE_SWEEP, `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 TypeError('INVALID_TYPE', '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.CACHE_SWEEP, `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 TypeError('INVALID_TYPE', 'filter', 'function');
}
const users = this.client.users.cache.sweep(filter);
this.client.emit(Events.CACHE_SWEEP, `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 TypeError('INVALID_TYPE', 'lifetime', 'number');
}
if (typeof getComparisonTimestamp !== 'function') {
throw new TypeError('INVALID_TYPE', 'getComparisonTimestamp', 'function');
}
if (typeof excludeFromSweep !== 'function') {
throw new TypeError('INVALID_TYPE', '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 TypeError('INVALID_TYPE', '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.CACHE_SWEEP, `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 TypeError('INVALID_TYPE', `sweepers.${key}`, 'object', true);
}
if (typeof props.interval !== 'number') {
throw new TypeError('INVALID_TYPE', `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 TypeError('INVALID_TYPE', `sweepers.${key}.lifetime`, 'number');
}
return;
}
if (typeof props.filter !== 'function') {
throw new TypeError('INVALID_TYPE', `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 TypeError('SWEEP_FILTER_RETURN');
this[sweepKey](sweepFn);
}, opts.interval * 1_000).unref();
}
}
module.exports = Sweepers;