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 +1 -1
- package/src/classes/Client.js +26 -0
- package/src/connection/rest/RestManager.js +183 -7
- package/src/index.js +1 -1
package/package.json
CHANGED
package/src/classes/Client.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
93
|
-
await this.sleep(
|
|
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 ${
|
|
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() +
|
|
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(
|
|
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
|
|
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.
|
|
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
|