discord-sb.js 1.0.5 → 1.0.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,21 +1,21 @@
1
- > [!IMPORTANT]
2
- > **This project is a fork of the [discord.js-selfbot-v13](https://github.com/aiko-chan-ai/discord.js-selfbot-v13) an archived project.**
1
+ [!IMPORTANT]
2
+ **This project is a fork of the [discord.js-selfbot-v13](https://github.com/aiko-chan-ai/discord.js-selfbot-v13) archived project.**
3
3
 
4
4
  # discord.js-selfbot-v13 (fork)
5
5
 
6
6
  Small additions focused on profile data and account integrations.
7
7
 
8
- ## Nouveautés
9
- - Les fetchs de users remontent désormais la bio/pronoms quand disponible (via `/users/{id}/profile`).
10
- - `client.user.fetchConnections({ includeMetadata })` pour récupérer vos connexions (Spotify, Steam, etc.) avec métadonnées quand l’API les expose.
8
+ ## What's New
9
+ - User fetches now include bio and pronouns when available (via `/users/{id}/profile`).
10
+ - `client.user.fetchConnections({ includeMetadata })` returns your connections (Spotify, Steam, etc.) with metadata when the API exposes it.
11
11
 
12
- ## Exemples rapides
12
+ ## Quick Examples
13
13
  ```js
14
- // Récupérer la bio d'un user
14
+ // Fetch a user's bio and pronouns
15
15
  const user = await client.users.fetch('123456789012345678');
16
16
  console.log(user.bio, user.pronouns);
17
17
 
18
- // Lister vos connexions avec métadonnées
18
+ // List your connections with metadata
19
19
  const connections = await client.user.fetchConnections();
20
20
  console.log(connections);
21
21
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discord-sb.js",
3
- "version": "1.0.5",
3
+ "version": "1.0.7",
4
4
  "description": "An unofficial discord.js fork for creating selfbots",
5
5
  "main": "./src/index.js",
6
6
  "types": "./typings/index.d.ts",
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "repository": {
31
31
  "type": "git",
32
- "url": "git+https://github.com/sqlu/discord-selfbot.js.git"
32
+ "url": "git+https://github.com/sqlu/discord-sb.js.git"
33
33
  },
34
34
  "keywords": [
35
35
  "discord.js",
@@ -47,9 +47,9 @@
47
47
  "author": "Snayz",
48
48
  "license": "GNU General Public License v3.0",
49
49
  "bugs": {
50
- "url": "https://github.com/sqlu/discord-selfbot.js/issues"
50
+ "url": "https://github.com/sqlu/discord-sb.js/issues"
51
51
  },
52
- "homepage": "https://github.com/sqlu/discord-selfbot.js#readme",
52
+ "homepage": "https://github.com/sqlu/discord-sb.js#readme",
53
53
  "dependencies": {
54
54
  "@discordjs/builders": "^1.6.3",
55
55
  "@discordjs/collection": "^2.1.1",
@@ -40,6 +40,7 @@ const DataResolver = require('../util/DataResolver');
40
40
  const Intents = require('../util/Intents');
41
41
  const DiscordAuthWebsocket = require('../util/RemoteAuth');
42
42
  const Sweepers = require('../util/Sweepers');
43
+ const TOKEN_PREFIX_REGEX = /^(Bot|Bearer)\s*/i;
43
44
 
44
45
  /**
45
46
  * The main hub for interacting with the Discord API, and the starting point for any bot.
@@ -184,6 +185,19 @@ class Client extends BaseClient {
184
185
  */
185
186
  this.settings = new ClientUserSettingManager(this);
186
187
 
188
+ this._emojiCacheDirty = true;
189
+ this._emojiManager = null;
190
+ this._invalidateEmojiCache = () => {
191
+ this._emojiCacheDirty = true;
192
+ };
193
+ this.on(Events.GUILD_CREATE, this._invalidateEmojiCache);
194
+ this.on(Events.GUILD_DELETE, this._invalidateEmojiCache);
195
+ this.on(Events.GUILD_EMOJI_CREATE, this._invalidateEmojiCache);
196
+ this.on(Events.GUILD_EMOJI_DELETE, this._invalidateEmojiCache);
197
+ this.on(Events.GUILD_EMOJI_UPDATE, this._invalidateEmojiCache);
198
+ this.on(Events.GUILD_EMOJIS_UPDATE, this._invalidateEmojiCache);
199
+ this.on(Events.GUILD_UNAVAILABLE, this._invalidateEmojiCache);
200
+
187
201
  Object.defineProperty(this, 'token', { writable: true });
188
202
  if (!this.token && 'DISCORD_TOKEN' in process.env) {
189
203
  /**
@@ -240,9 +254,20 @@ class Client extends BaseClient {
240
254
  * @readonly
241
255
  */
242
256
  get emojis() {
257
+ if (this._emojiCacheDirty || !this._emojiManager) {
258
+ this._emojiManager = this._buildEmojiCache();
259
+ this._emojiCacheDirty = false;
260
+ }
261
+ return this._emojiManager;
262
+ }
263
+
264
+ _buildEmojiCache() {
243
265
  const emojis = new BaseGuildEmojiManager(this);
244
266
  for (const guild of this.guilds.cache.values()) {
245
- if (guild.available) for (const emoji of guild.emojis.cache.values()) emojis.cache.set(emoji.id, emoji);
267
+ if (!guild.available) continue;
268
+ for (const emoji of guild.emojis.cache.values()) {
269
+ emojis.cache.set(emoji.id, emoji);
270
+ }
246
271
  }
247
272
  return emojis;
248
273
  }
@@ -274,7 +299,7 @@ class Client extends BaseClient {
274
299
  */
275
300
  async login(token = this.token) {
276
301
  if (!token || typeof token !== 'string') throw new Error('TOKEN_INVALID');
277
- this.token = token = token.replace(/^(Bot|Bearer)\s*/i, '');
302
+ this.token = token = token.replace(TOKEN_PREFIX_REGEX, '');
278
303
  this.emit(
279
304
  Events.DEBUG,
280
305
  `
@@ -363,6 +388,16 @@ class Client extends BaseClient {
363
388
  destroy() {
364
389
  super.destroy();
365
390
 
391
+ if (this._invalidateEmojiCache) {
392
+ this.off(Events.GUILD_CREATE, this._invalidateEmojiCache);
393
+ this.off(Events.GUILD_DELETE, this._invalidateEmojiCache);
394
+ this.off(Events.GUILD_EMOJI_CREATE, this._invalidateEmojiCache);
395
+ this.off(Events.GUILD_EMOJI_DELETE, this._invalidateEmojiCache);
396
+ this.off(Events.GUILD_EMOJI_UPDATE, this._invalidateEmojiCache);
397
+ this.off(Events.GUILD_EMOJIS_UPDATE, this._invalidateEmojiCache);
398
+ this.off(Events.GUILD_UNAVAILABLE, this._invalidateEmojiCache);
399
+ }
400
+
366
401
  for (const fn of this._cleanups) fn();
367
402
  this._cleanups.clear();
368
403
 
@@ -370,6 +405,8 @@ class Client extends BaseClient {
370
405
 
371
406
  this.sweepers.destroy();
372
407
  this.ws.destroy();
408
+ this._emojiManager = null;
409
+ this._emojiCacheDirty = true;
373
410
  this.token = null;
374
411
  }
375
412
 
@@ -841,7 +878,7 @@ class Client extends BaseClient {
841
878
  * @returns {Promise<boolean>}
842
879
  */
843
880
  async installUserApps(applicationId, scopes = ['applications.commands']) {
844
- const scope = Array.isArray(scopes) ? scopes.join(' ') : scopes;
881
+ const scope = typeof scopes === 'string' ? scopes : scopes.join(' ');
845
882
  await this.api.oauth2.authorize.post({
846
883
  query: {
847
884
  client_id: applicationId,
@@ -79,6 +79,7 @@ class WebSocketManager extends EventEmitter {
79
79
  * @name WebSocketManager#packetQueue
80
80
  */
81
81
  Object.defineProperty(this, 'packetQueue', { value: [] });
82
+ this._processingQueue = false;
82
83
 
83
84
  /**
84
85
  * The current status of this WebSocketManager
@@ -107,8 +108,11 @@ class WebSocketManager extends EventEmitter {
107
108
  * @readonly
108
109
  */
109
110
  get ping() {
110
- const sum = this.shards.reduce((a, b) => a + b.ping, 0);
111
- return sum / this.shards.size;
111
+ let total = 0;
112
+ for (const shard of this.shards.values()) {
113
+ total += shard.ping;
114
+ }
115
+ return this.shards.size ? total / this.shards.size : 0;
112
116
  }
113
117
 
114
118
  /**
@@ -315,8 +319,37 @@ class WebSocketManager extends EventEmitter {
315
319
  if (this.destroyed) return;
316
320
  this.debug(`Manager was destroyed. Called by:\n${new Error('MANAGER_DESTROYED').stack}`);
317
321
  this.destroyed = true;
318
- this.shardQueue.clear();
319
- for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false });
322
+ this.shardQueue.clear();
323
+ for (const shard of this.shards.values()) shard.destroy({ closeCode: 1_000, reset: true, emit: false, log: false });
324
+ }
325
+
326
+ _dispatchPacket(packet, shard) {
327
+ if (packet && PacketHandlers[packet.t]) {
328
+ PacketHandlers[packet.t](this.client, packet, shard);
329
+ } else if (packet) {
330
+ /**
331
+ * Emitted whenever a packet isn't handled.
332
+ * @event Client#unhandledPacket
333
+ * @param {Object} packet The packet (t: EVENT_NAME, d: any)
334
+ * @param {Number} shard The shard that received the packet (Shard 0)
335
+ */
336
+ this.client.emit(Events.UNHANDLED_PACKET, packet, shard);
337
+ }
338
+ }
339
+
340
+ _schedulePacketQueue() {
341
+ if (this._processingQueue || this.status !== Status.READY || !this.packetQueue.length) return;
342
+ this._processingQueue = true;
343
+ setImmediate(() => {
344
+ while (this.packetQueue.length && this.status === Status.READY) {
345
+ const { packet, shard } = this.packetQueue.shift();
346
+ this._dispatchPacket(packet, shard);
347
+ }
348
+ this._processingQueue = false;
349
+ if (this.packetQueue.length && this.status === Status.READY) {
350
+ this._schedulePacketQueue();
351
+ }
352
+ }).unref();
320
353
  }
321
354
 
322
355
  /**
@@ -327,30 +360,17 @@ class WebSocketManager extends EventEmitter {
327
360
  * @private
328
361
  */
329
362
  handlePacket(packet, shard) {
330
- if (packet && this.status !== Status.READY) {
331
- if (!BeforeReadyWhitelist.includes(packet.t)) {
332
- this.packetQueue.push({ packet, shard });
333
- return false;
334
- }
363
+ if (packet && this.status !== Status.READY && !BeforeReadyWhitelist.includes(packet.t)) {
364
+ this.packetQueue.push({ packet, shard });
365
+ return false;
335
366
  }
336
367
 
337
- if (this.packetQueue.length) {
338
- const item = this.packetQueue.shift();
339
- setImmediate(() => {
340
- this.handlePacket(item.packet, item.shard);
341
- }).unref();
368
+ if (packet) {
369
+ this._dispatchPacket(packet, shard);
342
370
  }
343
371
 
344
- if (packet && PacketHandlers[packet.t]) {
345
- PacketHandlers[packet.t](this.client, packet, shard);
346
- } else if (packet) {
347
- /**
348
- * Emitted whenever a packet isn't handled.
349
- * @event Client#unhandledPacket
350
- * @param {Object} packet The packet (t: EVENT_NAME, d: any)
351
- * @param {Number} shard The shard that received the packet (Shard 0)
352
- */
353
- this.client.emit(Events.UNHANDLED_PACKET, packet, shard);
372
+ if (this.status === Status.READY && this.packetQueue.length) {
373
+ this._schedulePacketQueue();
354
374
  }
355
375
 
356
376
  return true;
@@ -51,19 +51,19 @@ class CachedManager extends DataManager {
51
51
  }
52
52
 
53
53
  _add(data, cache = true, { id, extras = [] } = {}) {
54
- const existing = this.cache.get(id ?? data.id);
54
+ const entryId = id ?? data.id;
55
+ const existing = this.cache.get(entryId);
55
56
  if (existing) {
56
- if (cache) {
57
+ if (typeof existing._patch === 'function') {
57
58
  existing._patch(data);
58
- return existing;
59
59
  }
60
- const clone = existing._clone();
61
- clone._patch(data);
62
- return clone;
60
+ if (cache) this.cache.set(entryId, existing);
61
+ return existing;
63
62
  }
64
63
 
65
64
  const entry = this.holds ? new this.holds(this.client, data, ...extras) : data;
66
- if (cache) this.cache.set(id ?? entry.id, entry);
65
+ const cacheId = entryId ?? entry.id;
66
+ if (cache) this.cache.set(cacheId, entry);
67
67
  return entry;
68
68
  }
69
69
  }
@@ -131,7 +131,11 @@ class QuestManager extends BaseManager {
131
131
  * @returns {Quest[]}
132
132
  */
133
133
  getExpired(date = new Date()) {
134
- return this.list().filter(quest => quest.isExpired(date));
134
+ const expired = [];
135
+ for (const quest of this.cache.values()) {
136
+ if (quest.isExpired(date)) expired.push(quest);
137
+ }
138
+ return expired;
135
139
  }
136
140
 
137
141
  /**
@@ -139,7 +143,11 @@ class QuestManager extends BaseManager {
139
143
  * @returns {Quest[]}
140
144
  */
141
145
  getCompleted() {
142
- return this.list().filter(quest => quest.isCompleted());
146
+ const completed = [];
147
+ for (const quest of this.cache.values()) {
148
+ if (quest.isCompleted()) completed.push(quest);
149
+ }
150
+ return completed;
143
151
  }
144
152
 
145
153
  /**
@@ -147,7 +155,13 @@ class QuestManager extends BaseManager {
147
155
  * @returns {Quest[]}
148
156
  */
149
157
  getClaimable() {
150
- return this.list().filter(quest => quest.isCompleted() && !quest.hasClaimedRewards());
158
+ const claimable = [];
159
+ for (const quest of this.cache.values()) {
160
+ if (quest.isCompleted() && !quest.hasClaimedRewards()) {
161
+ claimable.push(quest);
162
+ }
163
+ }
164
+ return claimable;
151
165
  }
152
166
 
153
167
  /**
@@ -155,9 +169,13 @@ class QuestManager extends BaseManager {
155
169
  * @returns {Quest[]}
156
170
  */
157
171
  filterQuestsValid() {
158
- return this.list().filter(
159
- quest => quest.id !== '1412491570820812933' && !quest.isCompleted() && !quest.isExpired(),
160
- );
172
+ const valid = [];
173
+ for (const quest of this.cache.values()) {
174
+ if (quest.id !== '1412491570820812933' && !quest.isCompleted() && !quest.isExpired()) {
175
+ valid.push(quest);
176
+ }
177
+ }
178
+ return valid;
161
179
  }
162
180
 
163
181
  /**
@@ -75,13 +75,38 @@ class RequestHandler {
75
75
  }
76
76
 
77
77
  get limited() {
78
- return this.globalLimited || this.localLimited;
78
+ return this._getActiveRateLimit() !== null;
79
79
  }
80
80
 
81
81
  get _inactive() {
82
82
  return this.queue.remaining === 0 && !this.limited;
83
83
  }
84
84
 
85
+ _getActiveRateLimit(now = Date.now()) {
86
+ if (this.manager.globalRemaining <= 0 && now < this.manager.globalReset) {
87
+ const timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - now;
88
+ return { isGlobal: true, limit: this.manager.globalLimit, timeout };
89
+ }
90
+
91
+ if (this.remaining <= 0 && now < this.reset) {
92
+ const timeout = this.reset + this.manager.client.options.restTimeOffset - now;
93
+ return { isGlobal: false, limit: this.limit, timeout };
94
+ }
95
+
96
+ return null;
97
+ }
98
+
99
+ _getDelayPromise(isGlobal, timeout) {
100
+ const delay = Math.max(timeout, 0);
101
+ if (isGlobal) {
102
+ if (!this.manager.globalDelay) {
103
+ this.manager.globalDelay = this.globalDelayFor(delay);
104
+ }
105
+ return this.manager.globalDelay;
106
+ }
107
+ return sleep(delay);
108
+ }
109
+
85
110
  globalDelayFor(ms) {
86
111
  return new Promise(resolve => {
87
112
  setTimeout(() => {
@@ -116,32 +141,29 @@ class RequestHandler {
116
141
  }
117
142
 
118
143
  async execute(request, captchaKey, captchaToken) {
144
+ const hasRateLimitListener = this.manager.client.listenerCount(RATE_LIMIT) > 0;
145
+ const hasApiRequestListener = this.manager.client.listenerCount(API_REQUEST) > 0;
146
+ const hasApiResponseListener = this.manager.client.listenerCount(API_RESPONSE) > 0;
147
+ const invalidRequestInterval = this.manager.client.options.invalidRequestWarningInterval;
148
+ const hasInvalidRequestListener =
149
+ this.manager.client.listenerCount(INVALID_REQUEST_WARNING) > 0 && invalidRequestInterval > 0;
150
+
119
151
  /*
120
152
  * After calculations have been done, pre-emptively stop further requests
121
153
  * Potentially loop until this task can run if e.g. the global rate limit is hit twice
122
154
  */
123
- while (this.limited) {
124
- const isGlobal = this.globalLimited;
125
- let limit, timeout, delayPromise;
126
-
127
- if (isGlobal) {
128
- // Set the variables based on the global rate limit
129
- limit = this.manager.globalLimit;
130
- timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
131
- } else {
132
- // Set the variables based on the route-specific rate limit
133
- limit = this.limit;
134
- timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
135
- }
155
+ for (let rateLimitState = this._getActiveRateLimit(); rateLimitState; rateLimitState = this._getActiveRateLimit()) {
156
+ const { isGlobal, limit, timeout } = rateLimitState;
157
+ const safeTimeout = Math.max(timeout, 0);
136
158
 
137
- if (this.manager.client.listenerCount(RATE_LIMIT)) {
159
+ if (hasRateLimitListener) {
138
160
  /**
139
161
  * Emitted when the client hits a rate limit while making a request
140
162
  * @event BaseClient#rateLimit
141
163
  * @param {RateLimitData} rateLimitData Object containing the rate limit info
142
164
  */
143
165
  this.manager.client.emit(RATE_LIMIT, {
144
- timeout,
166
+ timeout: safeTimeout,
145
167
  limit,
146
168
  method: request.method,
147
169
  path: request.path,
@@ -150,27 +172,19 @@ class RequestHandler {
150
172
  });
151
173
  }
152
174
 
153
- if (isGlobal) {
154
- // If this is the first task to reach the global timeout, set the global delay
155
- if (!this.manager.globalDelay) {
156
- // The global delay function should clear the global delay state when it is resolved
157
- this.manager.globalDelay = this.globalDelayFor(timeout);
158
- }
159
- delayPromise = this.manager.globalDelay;
160
- } else {
161
- delayPromise = sleep(timeout);
162
- }
175
+ const delayPromise = this._getDelayPromise(isGlobal, safeTimeout);
163
176
 
164
177
  // Determine whether a RateLimitError should be thrown
165
- await this.onRateLimit(request, limit, timeout, isGlobal); // eslint-disable-line no-await-in-loop
178
+ await this.onRateLimit(request, limit, safeTimeout, isGlobal); // eslint-disable-line no-await-in-loop
166
179
 
167
180
  // Wait for the timeout to expire in order to avoid an actual 429
168
181
  await delayPromise; // eslint-disable-line no-await-in-loop
169
182
  }
170
183
 
171
184
  // As the request goes out, update the global usage information
172
- if (!this.manager.globalReset || this.manager.globalReset < Date.now()) {
173
- this.manager.globalReset = Date.now() + 1_000;
185
+ const now = Date.now();
186
+ if (!this.manager.globalReset || this.manager.globalReset < now) {
187
+ this.manager.globalReset = now + 1_000;
174
188
  this.manager.globalRemaining = this.manager.globalLimit;
175
189
  }
176
190
  this.manager.globalRemaining--;
@@ -185,7 +199,7 @@ class RequestHandler {
185
199
  * @property {number} retries The number of times this request has been attempted
186
200
  */
187
201
 
188
- if (this.manager.client.listenerCount(API_REQUEST)) {
202
+ if (hasApiRequestListener) {
189
203
  /**
190
204
  * Emitted before every API request.
191
205
  * This event can emit several times for the same request, e.g. when hitting a rate limit.
@@ -217,7 +231,7 @@ class RequestHandler {
217
231
  return this.execute(request);
218
232
  }
219
233
 
220
- if (this.manager.client.listenerCount(API_RESPONSE)) {
234
+ if (hasApiResponseListener) {
221
235
  /**
222
236
  * Emitted after every API request has received a response.
223
237
  * This event does not necessarily correlate to completion of the request, e.g. when hitting a rate limit.
@@ -278,16 +292,14 @@ class RequestHandler {
278
292
 
279
293
  // Count the invalid requests
280
294
  if (res.status === 401 || res.status === 403 || res.status === 429) {
281
- if (!invalidCountResetTime || invalidCountResetTime < Date.now()) {
282
- invalidCountResetTime = Date.now() + 1_000 * 60 * 10;
295
+ const invalidNow = Date.now();
296
+ if (!invalidCountResetTime || invalidCountResetTime < invalidNow) {
297
+ invalidCountResetTime = invalidNow + 1_000 * 60 * 10;
283
298
  invalidCount = 0;
284
299
  }
285
300
  invalidCount++;
286
301
 
287
- const emitInvalid =
288
- this.manager.client.listenerCount(INVALID_REQUEST_WARNING) &&
289
- this.manager.client.options.invalidRequestWarningInterval > 0 &&
290
- invalidCount % this.manager.client.options.invalidRequestWarningInterval === 0;
302
+ const emitInvalid = hasInvalidRequestListener && invalidCount % invalidRequestInterval === 0;
291
303
  if (emitInvalid) {
292
304
  /**
293
305
  * @typedef {Object} InvalidRequestWarningData
@@ -303,7 +315,7 @@ class RequestHandler {
303
315
  */
304
316
  this.manager.client.emit(INVALID_REQUEST_WARNING, {
305
317
  count: invalidCount,
306
- remainingTime: invalidCountResetTime - Date.now(),
318
+ remainingTime: invalidCountResetTime - invalidNow,
307
319
  });
308
320
  }
309
321
  }
@@ -318,17 +330,16 @@ class RequestHandler {
318
330
  if (res.status >= 400 && res.status < 500) {
319
331
  // Handle ratelimited requests
320
332
  if (res.status === 429) {
321
- const isGlobal = this.globalLimited;
322
- let limit, timeout;
323
- if (isGlobal) {
324
- // Set the variables based on the global rate limit
325
- limit = this.manager.globalLimit;
326
- timeout = this.manager.globalReset + this.manager.client.options.restTimeOffset - Date.now();
327
- } else {
328
- // Set the variables based on the route-specific rate limit
329
- limit = this.limit;
330
- timeout = this.reset + this.manager.client.options.restTimeOffset - Date.now();
331
- }
333
+ const rateLimitNow = Date.now();
334
+ const rateLimitState = this._getActiveRateLimit(rateLimitNow);
335
+ const isGlobal = rateLimitState?.isGlobal ?? this.globalLimited;
336
+ const limit = rateLimitState?.limit ?? (isGlobal ? this.manager.globalLimit : this.limit);
337
+ const computedTimeout =
338
+ rateLimitState?.timeout ??
339
+ (isGlobal
340
+ ? this.manager.globalReset + this.manager.client.options.restTimeOffset - rateLimitNow
341
+ : this.reset + this.manager.client.options.restTimeOffset - rateLimitNow);
342
+ const safeTimeout = Math.max(computedTimeout, 0);
332
343
 
333
344
  this.manager.client.emit(
334
345
  DEBUG,
@@ -338,11 +349,11 @@ class RequestHandler {
338
349
  Path : ${request.path}
339
350
  Route : ${request.route}
340
351
  Limit : ${limit}
341
- Timeout : ${timeout}ms
352
+ Timeout : ${safeTimeout}ms
342
353
  Sublimit: ${sublimitTimeout ? `${sublimitTimeout}ms` : 'None'}`,
343
354
  );
344
355
 
345
- await this.onRateLimit(request, limit, timeout, isGlobal);
356
+ await this.onRateLimit(request, limit, safeTimeout, isGlobal);
346
357
 
347
358
  // If caused by a sublimit, wait it out here so other requests on the route can be handled
348
359
  if (sublimitTimeout) {
@@ -1670,16 +1670,19 @@ class Guild extends AnonymousGuild {
1670
1670
  * guild.markRead([
1671
1671
  * { channel_id: '123456789012345678', message_id: '987654321098765432' }
1672
1672
  * ]);
1673
- */
1673
+ */
1674
1674
  async markRead(readStates = []) {
1675
1675
  // If no readStates provided, get all channels in guild
1676
1676
  if (readStates.length === 0) {
1677
- const channels = this.channels.cache.filter(c => c.isTextBased());
1678
- readStates = channels.map(channel => ({
1679
- channel_id: channel.id,
1680
- message_id: channel.lastMessageId || '0',
1681
- read_state_type: 0,
1682
- }));
1677
+ readStates = [];
1678
+ for (const channel of this.channels.cache.values()) {
1679
+ if (!channel.isTextBased()) continue;
1680
+ readStates.push({
1681
+ channel_id: channel.id,
1682
+ message_id: channel.lastMessageId || '0',
1683
+ read_state_type: 0,
1684
+ });
1685
+ }
1683
1686
  }
1684
1687
 
1685
1688
  const data = await this.client.api['read-states']['ack-bulk'].post({
@@ -142,7 +142,15 @@ class Presence extends Base {
142
142
 
143
143
  _clone() {
144
144
  const clone = Object.assign(Object.create(this), this);
145
- clone.activities = this.activities.map(activity => activity._clone());
145
+ if (!this.activities.length) {
146
+ clone.activities = this.activities;
147
+ return clone;
148
+ }
149
+ const clonedActivities = [];
150
+ for (const activity of this.activities) {
151
+ clonedActivities.push(typeof activity._clone === 'function' ? activity._clone() : activity);
152
+ }
153
+ clone.activities = clonedActivities;
146
154
  return clone;
147
155
  }
148
156
 
@@ -33,29 +33,22 @@ class Sweepers {
33
33
  * A record of interval timeout that is used to sweep the indicated items, or null if not being swept
34
34
  * @type {Object<SweeperKey, ?Timeout>}
35
35
  */
36
- this.intervals = Object.fromEntries(SweeperKeys.map(key => [key, null]));
36
+ this.intervals = {};
37
+ for (const key of SweeperKeys) {
38
+ this.intervals[key] = null;
39
+ }
40
+
41
+ /**
42
+ * Cached validated options to avoid recalculating filters
43
+ * @type {Map<string, Object>}
44
+ * @private
45
+ */
46
+ this._validatedOptions = new Map();
37
47
 
38
48
  for (const key of SweeperKeys) {
39
49
  if (!(key in options)) continue;
40
50
 
41
- this._validateProperties(key);
42
-
43
- const clonedOptions = { ...this.options[key] };
44
-
45
- // Handle cases that have a "lifetime"
46
- if (!('filter' in clonedOptions)) {
47
- switch (key) {
48
- case 'invites':
49
- clonedOptions.filter = this.constructor.expiredInviteSweepFilter(clonedOptions.lifetime);
50
- break;
51
- case 'messages':
52
- clonedOptions.filter = this.constructor.outdatedMessageSweepFilter(clonedOptions.lifetime);
53
- break;
54
- case 'threads':
55
- clonedOptions.filter = this.constructor.archivedThreadSweepFilter(clonedOptions.lifetime);
56
- }
57
- }
58
-
51
+ const clonedOptions = this._getValidatedOptions(key);
59
52
  this._initInterval(key, `sweep${key[0].toUpperCase()}${key.slice(1)}`, clonedOptions);
60
53
  }
61
54
  }
@@ -124,6 +117,14 @@ class Sweepers {
124
117
  return this._sweepGuildDirectProp('members', filter, { outputName: 'guild members' }).items;
125
118
  }
126
119
 
120
+ _getTextChannels() {
121
+ const textChannels = [];
122
+ for (const channel of this.client.channels.cache.values()) {
123
+ if (channel.isText()) textChannels.push(channel);
124
+ }
125
+ return textChannels;
126
+ }
127
+
127
128
  /**
128
129
  * Sweeps all text-based channels' messages and removes the ones which are indicated by the filter.
129
130
  * @param {Function} filter The function used to determine which messages will be removed from the caches.
@@ -142,15 +143,13 @@ class Sweepers {
142
143
  if (typeof filter !== 'function') {
143
144
  throw new TypeError('INVALID_TYPE', 'filter', 'function');
144
145
  }
145
- let channels = 0;
146
+ const textChannels = this._getTextChannels();
146
147
  let messages = 0;
147
148
 
148
- for (const channel of this.client.channels.cache.values()) {
149
- if (!channel.isText()) continue;
150
-
151
- channels++;
149
+ for (const channel of textChannels) {
152
150
  messages += channel.messages.cache.sweep(filter);
153
151
  }
152
+ const channels = textChannels.length;
154
153
  this.client.emit(Events.CACHE_SWEEP, `Swept ${messages} messages in ${channels} text-based channels.`);
155
154
  return messages;
156
155
  }
@@ -173,19 +172,19 @@ class Sweepers {
173
172
  if (typeof filter !== 'function') {
174
173
  throw new TypeError('INVALID_TYPE', 'filter', 'function');
175
174
  }
176
- let channels = 0;
175
+ const textChannels = this._getTextChannels();
177
176
  let messages = 0;
178
177
  let reactions = 0;
179
178
 
180
- for (const channel of this.client.channels.cache.values()) {
181
- if (!channel.isText()) continue;
182
- channels++;
183
-
179
+ for (const channel of textChannels) {
184
180
  for (const message of channel.messages.cache.values()) {
181
+ const reactionCache = message.reactions?.cache;
182
+ if (!reactionCache?.size) continue;
185
183
  messages++;
186
- reactions += message.reactions.cache.sweep(filter);
184
+ reactions += reactionCache.sweep(filter);
187
185
  }
188
186
  }
187
+ const channels = textChannels.length;
189
188
  this.client.emit(
190
189
  Events.CACHE_SWEEP,
191
190
  `Swept ${reactions} reactions on ${messages} messages in ${channels} text-based channels.`,
@@ -390,6 +389,34 @@ class Sweepers {
390
389
  * @private
391
390
  */
392
391
 
392
+ _getValidatedOptions(key) {
393
+ if (this._validatedOptions.has(key)) {
394
+ return this._validatedOptions.get(key);
395
+ }
396
+
397
+ this._validateProperties(key);
398
+ const clonedOptions = { ...this.options[key] };
399
+
400
+ if (!('filter' in clonedOptions)) {
401
+ switch (key) {
402
+ case 'invites':
403
+ clonedOptions.filter = this.constructor.expiredInviteSweepFilter(clonedOptions.lifetime);
404
+ break;
405
+ case 'messages':
406
+ clonedOptions.filter = this.constructor.outdatedMessageSweepFilter(clonedOptions.lifetime);
407
+ break;
408
+ case 'threads':
409
+ clonedOptions.filter = this.constructor.archivedThreadSweepFilter(clonedOptions.lifetime);
410
+ break;
411
+ default:
412
+ break;
413
+ }
414
+ }
415
+
416
+ this._validatedOptions.set(key, clonedOptions);
417
+ return clonedOptions;
418
+ }
419
+
393
420
  /**
394
421
  * Sweep a direct sub property of all guilds
395
422
  * @param {string} key The name of the property
@@ -436,7 +436,7 @@ export const enum ApplicationType {
436
436
  CREATOR_MONETIZATION = 4,
437
437
  }
438
438
 
439
- export enum ClientEvents {
439
+ export const enum ClientEvents {
440
440
  RateLimit = 'rateLimit',
441
441
  InvalidRequestWarning = 'invalidRequestWarning',
442
442
  ApiResponse = 'apiResponse',