discord-self-lite 0.1.6 → 0.1.8

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discord-self-lite",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "Lightweight Discord selfbot library",
5
5
  "keywords": [
6
6
  "discord",
@@ -191,6 +191,32 @@ class Client extends EventEmitter {
191
191
  return this.token;
192
192
  }
193
193
 
194
+ /**
195
+ * Get rate limit status for debugging
196
+ * @param {string} endpoint - The API endpoint
197
+ * @param {string} method - The HTTP method (default: 'GET')
198
+ * @returns {object} Rate limit information
199
+ */
200
+ getRateLimitStatus(endpoint, method = "GET") {
201
+ if (!this.rest) {
202
+ throw new Error("Client is not logged in");
203
+ }
204
+ return this.rest.getRateLimitStatus(endpoint, method);
205
+ }
206
+
207
+ /**
208
+ * Check if a route is currently rate limited
209
+ * @param {string} endpoint - The API endpoint
210
+ * @param {string} method - The HTTP method (default: 'GET')
211
+ * @returns {boolean} True if rate limited
212
+ */
213
+ isRateLimited(endpoint, method = "GET") {
214
+ if (!this.rest) {
215
+ return false;
216
+ }
217
+ return this.rest.isRateLimited(endpoint, method);
218
+ }
219
+
194
220
  /**
195
221
  * Destroy the client and clean up resources
196
222
  * @returns {void}
@@ -23,6 +23,9 @@ class RestManager {
23
23
  this.globalRateLimit = null; // { reset }
24
24
  this.requestQueue = [];
25
25
  this.processingQueue = false;
26
+
27
+ // Circuit breaker state for heavily rate-limited routes
28
+ this.circuitBreakers = new Map(); // routeKey -> { failures, lastFailure, openUntil }
26
29
  }
27
30
 
28
31
  /**
@@ -33,7 +36,56 @@ class RestManager {
33
36
  * @throws {Error} If the request fails
34
37
  */
35
38
  async request(endpoint, options = {}) {
39
+ // Check rate limits before queuing the request with more aggressive logic
40
+ const method = options.method || "GET";
41
+ const routeKey = this.getRouteKey(endpoint, method);
42
+
43
+ // Check circuit breaker - reject immediately if route is temporarily disabled
44
+ const circuitBreaker = this.circuitBreakers.get(routeKey);
45
+ if (circuitBreaker && circuitBreaker.openUntil > Date.now()) {
46
+ const remainingTime = circuitBreaker.openUntil - Date.now();
47
+ throw new Error(
48
+ `Circuit breaker open for ${routeKey}. Retry in ${remainingTime}ms`,
49
+ );
50
+ }
51
+
52
+ if (this.isRateLimited(endpoint, method)) {
53
+ const rateLimit = this.rateLimits.get(routeKey);
54
+ const globalLimited =
55
+ this.globalRateLimit && Date.now() < this.globalRateLimit.reset;
56
+
57
+ // Calculate delay with minimum 2s buffer and more conservative timing
58
+ const baseDelay = globalLimited
59
+ ? this.globalRateLimit.reset - Date.now()
60
+ : rateLimit
61
+ ? rateLimit.reset - Date.now()
62
+ : 1000;
63
+
64
+ // Add buffer time and ensure minimum delay
65
+ const delay = Math.max(baseDelay + 2000, 3000); // Minimum 3s delay
66
+
67
+ console.log(
68
+ `🚫 Pre-emptive rate limit wait: ${delay}ms for ${routeKey} (${endpoint})`,
69
+ );
70
+ await this.sleep(delay);
71
+
72
+ // Double-check after waiting - if still limited, reject immediately
73
+ if (this.isRateLimited(endpoint, method)) {
74
+ throw new Error(`Endpoint still rate limited after wait: ${routeKey}`);
75
+ }
76
+ }
77
+
36
78
  return new Promise((resolve, reject) => {
79
+ // Prevent queue from growing too large during rate limit storms
80
+ if (this.requestQueue.length >= 50) {
81
+ reject(
82
+ new Error(
83
+ `Request queue full (${this.requestQueue.length} requests). Rate limiting too aggressive.`,
84
+ ),
85
+ );
86
+ return;
87
+ }
88
+
37
89
  this.requestQueue.push({
38
90
  endpoint,
39
91
  options,
@@ -55,10 +107,20 @@ class RestManager {
55
107
  return;
56
108
  }
57
109
 
110
+ // Check if we're globally rate limited before starting
111
+ if (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
112
+ const delay = this.globalRateLimit.reset - Date.now();
113
+ console.log(
114
+ `⏳ Global rate limit active, waiting ${delay}ms before processing queue`,
115
+ );
116
+ setTimeout(() => this.processQueue(), delay);
117
+ return;
118
+ }
119
+
58
120
  this.processingQueue = true;
59
121
 
60
122
  while (this.requestQueue.length > 0) {
61
- // Check global rate limit
123
+ // Double-check global rate limit for each request
62
124
  if (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
63
125
  const delay = this.globalRateLimit.reset - Date.now();
64
126
  console.log(`⏳ Global rate limit active, waiting ${delay}ms`);
@@ -84,13 +146,21 @@ class RestManager {
84
146
  `⏳ Route rate limit for ${routeKey} (${endpoint}), waiting ${delay}ms`,
85
147
  );
86
148
  await this.sleep(delay);
149
+
150
+ // After waiting, check if we still have remaining requests
151
+ const updatedRateLimit = this.rateLimits.get(routeKey);
152
+ if (updatedRateLimit && updatedRateLimit.remaining <= 0) {
153
+ // Still rate limited, put request back at front of queue
154
+ this.requestQueue.unshift(request);
155
+ continue;
156
+ }
87
157
  }
88
158
 
89
159
  const result = await this.makeRequest(endpoint, options);
90
160
  resolve(result);
91
161
 
92
- // Small delay between requests to be nice to Discord
93
- await this.sleep(100);
162
+ // Increased delay between requests to be more conservative with rate limits
163
+ await this.sleep(500);
94
164
  } catch (error) {
95
165
  reject(error);
96
166
  }
@@ -134,17 +204,52 @@ class RestManager {
134
204
  response.headers.get("x-ratelimit-global") === "true";
135
205
  const routeKey = this.getRouteKey(endpoint, options.method || "GET");
136
206
 
207
+ // Exponential backoff: increase delay based on attempt number
208
+ const backoffDelay = retryAfter * Math.pow(1.5, attempt);
209
+
137
210
  console.log(
138
211
  `🚫 Rate limited! ${
139
212
  isGlobal ? "Global" : `Route (${routeKey})`
140
- } limit for ${endpoint}, retry after ${retryAfter}ms`,
213
+ } limit for ${endpoint}, retry after ${backoffDelay}ms (attempt ${attempt + 1})`,
141
214
  );
142
215
 
216
+ // Circuit breaker: track consecutive failures
217
+ if (!isGlobal) {
218
+ const breaker = this.circuitBreakers.get(routeKey) || {
219
+ failures: 0,
220
+ lastFailure: 0,
221
+ openUntil: 0,
222
+ };
223
+ breaker.failures++;
224
+ breaker.lastFailure = Date.now();
225
+
226
+ // Open circuit breaker after 3 consecutive failures within 30 seconds
227
+ if (
228
+ breaker.failures >= 3 &&
229
+ Date.now() - breaker.lastFailure < 30000
230
+ ) {
231
+ breaker.openUntil = Date.now() + 60000; // Open for 60 seconds
232
+ console.log(
233
+ `🔌 Circuit breaker OPENED for ${routeKey} - too many rate limits`,
234
+ );
235
+ }
236
+
237
+ this.circuitBreakers.set(routeKey, breaker);
238
+ }
239
+
143
240
  if (isGlobal) {
144
- this.globalRateLimit = { reset: Date.now() + retryAfter };
241
+ this.globalRateLimit = { reset: Date.now() + backoffDelay };
242
+ } else {
243
+ // Update route-specific rate limit for 429 responses
244
+ this.rateLimits.set(routeKey, {
245
+ remaining: 0,
246
+ reset: Date.now() + backoffDelay,
247
+ limit: this.rateLimits.get(routeKey)?.limit || 5, // Default limit if unknown
248
+ updatedAt: Date.now(),
249
+ });
145
250
  }
146
251
 
147
- await this.sleep(retryAfter);
252
+ await this.sleep(backoffDelay);
148
253
  attempt++;
149
254
  continue;
150
255
  }
@@ -158,9 +263,16 @@ class RestManager {
158
263
 
159
264
  // Handle 204 No Content (successful but no body)
160
265
  if (response.status === 204) {
266
+ // Reset circuit breaker on successful response
267
+ const routeKey = this.getRouteKey(endpoint, options.method || "GET");
268
+ this.circuitBreakers.delete(routeKey);
161
269
  return null;
162
270
  }
163
271
 
272
+ // Reset circuit breaker on successful response
273
+ const routeKey = this.getRouteKey(endpoint, options.method || "GET");
274
+ this.circuitBreakers.delete(routeKey);
275
+
164
276
  return await response.json();
165
277
  } catch (error) {
166
278
  if (attempt === maxRetries - 1) {
@@ -232,16 +344,80 @@ class RestManager {
232
344
  this.rateLimits.delete(key);
233
345
  }
234
346
  }
347
+
348
+ // Clean up expired circuit breakers
349
+ for (const [key, breaker] of this.circuitBreakers.entries()) {
350
+ if (now > breaker.openUntil) {
351
+ this.circuitBreakers.delete(key);
352
+ }
353
+ }
235
354
  }
236
355
 
237
356
  /**
238
- * Sleep for a given duration
357
+ * Sleep for a specified number of milliseconds
239
358
  * @private
359
+ * @param {number} ms - Milliseconds to sleep
360
+ * @returns {Promise<void>}
240
361
  */
241
362
  sleep(ms) {
242
363
  return new Promise((resolve) => setTimeout(resolve, ms));
243
364
  }
244
365
 
366
+ /**
367
+ * Check if a route is currently rate limited
368
+ * @param {string} endpoint - The API endpoint
369
+ * @param {string} method - The HTTP method
370
+ * @returns {boolean} True if rate limited
371
+ */
372
+ isRateLimited(endpoint, method = "GET") {
373
+ // Check global rate limit
374
+ if (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
375
+ return true;
376
+ }
377
+
378
+ // Check route-specific rate limit
379
+ const routeKey = this.getRouteKey(endpoint, method);
380
+ const rateLimit = this.rateLimits.get(routeKey);
381
+
382
+ return (
383
+ rateLimit && rateLimit.remaining <= 0 && Date.now() < rateLimit.reset
384
+ );
385
+ }
386
+
387
+ /**
388
+ * Get rate limit status for debugging
389
+ * @param {string} endpoint - The API endpoint
390
+ * @param {string} method - The HTTP method
391
+ * @returns {object} Rate limit information
392
+ */
393
+ getRateLimitStatus(endpoint, method = "GET") {
394
+ const routeKey = this.getRouteKey(endpoint, method);
395
+ const rateLimit = this.rateLimits.get(routeKey);
396
+
397
+ return {
398
+ routeKey,
399
+ endpoint,
400
+ method,
401
+ isGloballyLimited:
402
+ this.globalRateLimit && Date.now() < this.globalRateLimit.reset,
403
+ globalReset: this.globalRateLimit
404
+ ? new Date(this.globalRateLimit.reset)
405
+ : null,
406
+ isRouteLimited:
407
+ rateLimit && rateLimit.remaining <= 0 && Date.now() < rateLimit.reset,
408
+ routeLimit: rateLimit
409
+ ? {
410
+ remaining: rateLimit.remaining,
411
+ limit: rateLimit.limit,
412
+ reset: new Date(rateLimit.reset),
413
+ resetIn: Math.max(0, rateLimit.reset - Date.now()),
414
+ }
415
+ : null,
416
+ queueLength: this.requestQueue.length,
417
+ isProcessing: this.processingQueue,
418
+ };
419
+ }
420
+
245
421
  /**
246
422
  * Send a message to a channel
247
423
  * @param {string} channelId - The channel ID
package/src/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * discord-self-lite - A lightweight Discord selfbot library
3
3
  * @module discord-self-lite
4
- * @version 0.1.0
4
+ * @version 0.1.7
5
5
  * @description A modern, lightweight Discord selfbot library with webhook support, built for Node.js 22+ with zero external dependencies (except ws for WebSocket connections).
6
6
  *
7
7
  * @example