discord-self-lite 0.1.16 → 0.1.18
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,63 +1,64 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "discord-self-lite",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "Lightweight Discord selfbot library",
|
|
5
|
-
"keywords": [
|
|
6
|
-
"discord",
|
|
7
|
-
"selfbot",
|
|
8
|
-
"bot",
|
|
9
|
-
"api",
|
|
10
|
-
"websocket",
|
|
11
|
-
"discord-api",
|
|
12
|
-
"discord-bot",
|
|
13
|
-
"discord-selfbot",
|
|
14
|
-
"lightweight",
|
|
15
|
-
"nodejs"
|
|
16
|
-
],
|
|
17
|
-
"main": "src/index.js",
|
|
18
|
-
"files": [
|
|
19
|
-
"src",
|
|
20
|
-
"README.md",
|
|
21
|
-
"LICENSE"
|
|
22
|
-
],
|
|
23
|
-
"exports": {
|
|
24
|
-
".": "./src/index.js",
|
|
25
|
-
"./classes/Client": "./src/classes/Client.js",
|
|
26
|
-
"./classes/WebhookClient": "./src/connection/webhook/WebhookClient.js"
|
|
27
|
-
},
|
|
28
|
-
"engines": {
|
|
29
|
-
"node": ">=18.13.0"
|
|
30
|
-
},
|
|
31
|
-
"scripts": {
|
|
32
|
-
"test": "
|
|
33
|
-
"
|
|
34
|
-
"
|
|
35
|
-
"
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
"
|
|
44
|
-
"
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
"
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"eslint-
|
|
61
|
-
"prettier": "^
|
|
62
|
-
|
|
63
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "discord-self-lite",
|
|
3
|
+
"version": "0.1.18",
|
|
4
|
+
"description": "Lightweight Discord selfbot library",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"discord",
|
|
7
|
+
"selfbot",
|
|
8
|
+
"bot",
|
|
9
|
+
"api",
|
|
10
|
+
"websocket",
|
|
11
|
+
"discord-api",
|
|
12
|
+
"discord-bot",
|
|
13
|
+
"discord-selfbot",
|
|
14
|
+
"lightweight",
|
|
15
|
+
"nodejs"
|
|
16
|
+
],
|
|
17
|
+
"main": "src/index.js",
|
|
18
|
+
"files": [
|
|
19
|
+
"src",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
23
|
+
"exports": {
|
|
24
|
+
".": "./src/index.js",
|
|
25
|
+
"./classes/Client": "./src/classes/Client.js",
|
|
26
|
+
"./classes/WebhookClient": "./src/connection/webhook/WebhookClient.js"
|
|
27
|
+
},
|
|
28
|
+
"engines": {
|
|
29
|
+
"node": ">=18.13.0"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"test": "node --test tests/*.test.js",
|
|
33
|
+
"lint": "eslint src tests eslint.config.js && prettier --check src tests eslint.config.js package.json package-lock.json .github/workflows",
|
|
34
|
+
"format": "prettier --write .",
|
|
35
|
+
"fix": "eslint . --fix && prettier --write .",
|
|
36
|
+
"docs": "echo \"📚 Documentation available at docs/README.md\""
|
|
37
|
+
},
|
|
38
|
+
"repository": {
|
|
39
|
+
"type": "git",
|
|
40
|
+
"url": "git+https://github.com/kyan0045/discord-self-lite.git"
|
|
41
|
+
},
|
|
42
|
+
"author": {
|
|
43
|
+
"name": "Kyan Bosman",
|
|
44
|
+
"email": "contact@kyanbosman.com",
|
|
45
|
+
"url": "https://github.com/kyan0045"
|
|
46
|
+
},
|
|
47
|
+
"license": "GPL-3.0-or-later",
|
|
48
|
+
"bugs": {
|
|
49
|
+
"url": "https://github.com/kyan0045/discord-self-lite/issues"
|
|
50
|
+
},
|
|
51
|
+
"homepage": "https://github.com/kyan0045/discord-self-lite#readme",
|
|
52
|
+
"funding": {
|
|
53
|
+
"type": "github",
|
|
54
|
+
"url": "https://github.com/sponsors/kyan0045"
|
|
55
|
+
},
|
|
56
|
+
"dependencies": {
|
|
57
|
+
"ws": "^8.21.1"
|
|
58
|
+
},
|
|
59
|
+
"devDependencies": {
|
|
60
|
+
"eslint-config-prettier": "^10.1.8",
|
|
61
|
+
"eslint-plugin-prettier": "^5.5.4",
|
|
62
|
+
"prettier": "^3.6.2"
|
|
63
|
+
}
|
|
64
|
+
}
|
package/src/classes/Message.js
CHANGED
|
@@ -93,6 +93,15 @@ class Message {
|
|
|
93
93
|
return this;
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Delete this message
|
|
98
|
+
* @returns {Promise<Message>} This message instance after deletion
|
|
99
|
+
*/
|
|
100
|
+
async delete() {
|
|
101
|
+
await this.client.rest.deleteMessage(this.channelId, this.id);
|
|
102
|
+
return this;
|
|
103
|
+
}
|
|
104
|
+
|
|
96
105
|
/**
|
|
97
106
|
* Get the guild this message was sent in
|
|
98
107
|
* @returns {Guild|null} The guild instance, or null if message was sent in DM
|
|
@@ -23,14 +23,9 @@ class RestManager {
|
|
|
23
23
|
// Rate limiting state
|
|
24
24
|
this.rateLimits = new Map(); // endpoint -> { remaining, reset, retryAfter }
|
|
25
25
|
this.globalRateLimit = null; // { reset }
|
|
26
|
-
this.
|
|
27
|
-
this.
|
|
28
|
-
|
|
29
|
-
// Circuit breaker state for heavily rate-limited routes
|
|
30
|
-
this.circuitBreakers = new Map(); // routeKey -> { failures, lastFailure, openUntil }
|
|
31
|
-
|
|
32
|
-
// Suspended routes - temporarily disabled endpoints
|
|
33
|
-
this.suspendedRoutes = new Map(); // routeKey -> { suspendedUntil, reason }
|
|
26
|
+
this.globalDelay = null;
|
|
27
|
+
this.requestQueues = new Map(); // routeKey -> queued requests
|
|
28
|
+
this.processingRoutes = new Set();
|
|
34
29
|
}
|
|
35
30
|
|
|
36
31
|
/**
|
|
@@ -41,65 +36,24 @@ class RestManager {
|
|
|
41
36
|
* @throws {Error} If the request fails
|
|
42
37
|
*/
|
|
43
38
|
async request(endpoint, options = {}) {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const circuitBreaker = this.circuitBreakers.get(routeKey);
|
|
50
|
-
if (circuitBreaker && circuitBreaker.openUntil > Date.now()) {
|
|
51
|
-
const remainingTime = circuitBreaker.openUntil - Date.now();
|
|
52
|
-
console.log(
|
|
53
|
-
`🚫 Request dropped: Circuit breaker open for ${routeKey} (${Math.round(remainingTime / 1000)}s remaining)`,
|
|
54
|
-
);
|
|
55
|
-
return Promise.resolve(null);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// Check if route is suspended - silently reject to avoid spam
|
|
59
|
-
const suspendedRoute = this.suspendedRoutes.get(routeKey);
|
|
60
|
-
if (suspendedRoute && suspendedRoute.suspendedUntil > Date.now()) {
|
|
61
|
-
const remainingTime = suspendedRoute.suspendedUntil - Date.now();
|
|
62
|
-
console.log(
|
|
63
|
-
`🚫 Request dropped: Route ${routeKey} is suspended for ${Math.round(remainingTime / 1000)}s`,
|
|
64
|
-
);
|
|
65
|
-
return Promise.resolve(null);
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
// Check rate limits - if rate limited, suspend immediately instead of waiting
|
|
69
|
-
if (this.isRateLimited(endpoint, method)) {
|
|
70
|
-
const suspensionTime = 2 * 60 * 1000; // 2 minutes
|
|
71
|
-
this.suspendedRoutes.set(routeKey, {
|
|
72
|
-
suspendedUntil: Date.now() + suspensionTime,
|
|
73
|
-
reason: "Rate limited - pre-emptive suspension",
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
console.log(
|
|
77
|
-
`⏸️ Suspending route ${routeKey} for 2 minutes (pre-emptive)`,
|
|
78
|
-
);
|
|
79
|
-
|
|
80
|
-
// Clear queued requests for this route
|
|
81
|
-
const clearedCount = this.clearQueuedRequestsForRoute(routeKey);
|
|
82
|
-
if (clearedCount > 0) {
|
|
83
|
-
console.log(
|
|
84
|
-
`🗑️ Cleared ${clearedCount} queued requests for suspended route ${routeKey}`,
|
|
85
|
-
);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
console.log(`🚫 Request dropped: Route suspended for ${routeKey}`);
|
|
89
|
-
return Promise.resolve(null);
|
|
39
|
+
const routeKey = this.getRouteKey(endpoint, options.method || "GET");
|
|
40
|
+
let queue = this.requestQueues.get(routeKey);
|
|
41
|
+
if (!queue) {
|
|
42
|
+
queue = [];
|
|
43
|
+
this.requestQueues.set(routeKey, queue);
|
|
90
44
|
}
|
|
91
45
|
|
|
92
46
|
return new Promise((resolve, reject) => {
|
|
93
47
|
// Prevent queue from growing too large during rate limit storms
|
|
94
|
-
if (
|
|
48
|
+
if (queue.length >= 50) {
|
|
95
49
|
console.log(
|
|
96
|
-
`🚫 Request dropped: Queue full (${
|
|
50
|
+
`🚫 Request dropped: Queue full for ${routeKey} (${queue.length} requests)`,
|
|
97
51
|
);
|
|
98
52
|
resolve(null);
|
|
99
53
|
return;
|
|
100
54
|
}
|
|
101
55
|
|
|
102
|
-
|
|
56
|
+
queue.push({
|
|
103
57
|
endpoint,
|
|
104
58
|
options,
|
|
105
59
|
resolve,
|
|
@@ -107,7 +61,7 @@ class RestManager {
|
|
|
107
61
|
timestamp: Date.now(),
|
|
108
62
|
});
|
|
109
63
|
|
|
110
|
-
this.processQueue();
|
|
64
|
+
this.processQueue(routeKey);
|
|
111
65
|
});
|
|
112
66
|
}
|
|
113
67
|
|
|
@@ -115,109 +69,68 @@ class RestManager {
|
|
|
115
69
|
* Process the request queue with rate limiting
|
|
116
70
|
* @private
|
|
117
71
|
*/
|
|
118
|
-
async processQueue() {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Check if we're globally rate limited before starting
|
|
124
|
-
if (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
|
|
125
|
-
const delay = this.globalRateLimit.reset - Date.now();
|
|
126
|
-
console.log(
|
|
127
|
-
`⏳ Global rate limit active, waiting ${delay}ms before processing queue`,
|
|
128
|
-
);
|
|
129
|
-
setTimeout(() => this.processQueue(), delay);
|
|
72
|
+
async processQueue(routeKey) {
|
|
73
|
+
const queue = this.requestQueues.get(routeKey);
|
|
74
|
+
if (!queue || queue.length === 0 || this.processingRoutes.has(routeKey)) {
|
|
130
75
|
return;
|
|
131
76
|
}
|
|
132
77
|
|
|
133
|
-
this.
|
|
134
|
-
|
|
135
|
-
// Filter out suspended routes before processing any requests
|
|
136
|
-
this.requestQueue = this.requestQueue.filter((request) => {
|
|
137
|
-
const routeKey = this.getRouteKey(
|
|
138
|
-
request.endpoint,
|
|
139
|
-
request.options.method || "GET",
|
|
140
|
-
);
|
|
141
|
-
const suspendedRoute = this.suspendedRoutes.get(routeKey);
|
|
78
|
+
this.processingRoutes.add(routeKey);
|
|
142
79
|
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
console.log(
|
|
146
|
-
`🚫 Queued request dropped: Route ${routeKey} is suspended`,
|
|
147
|
-
);
|
|
148
|
-
request.resolve(null);
|
|
149
|
-
return false; // Remove from queue
|
|
150
|
-
}
|
|
80
|
+
while (queue.length > 0) {
|
|
81
|
+
await this.waitForGlobalRateLimit();
|
|
151
82
|
|
|
152
|
-
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
while (this.requestQueue.length > 0) {
|
|
156
|
-
// Double-check global rate limit for each request
|
|
157
|
-
if (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
|
|
158
|
-
const delay = this.globalRateLimit.reset - Date.now();
|
|
159
|
-
console.log(`⏳ Global rate limit active, waiting ${delay}ms`);
|
|
160
|
-
await this.sleep(delay);
|
|
161
|
-
this.globalRateLimit = null;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
const request = this.requestQueue.shift();
|
|
83
|
+
const request = queue.shift();
|
|
165
84
|
const { endpoint, options, resolve, reject } = request;
|
|
166
85
|
|
|
167
86
|
try {
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
// Check suspension status before processing
|
|
171
|
-
const suspendedRoute = this.suspendedRoutes.get(routeKey);
|
|
172
|
-
if (suspendedRoute && suspendedRoute.suspendedUntil > Date.now()) {
|
|
173
|
-
console.log(
|
|
174
|
-
`🚫 Processing request dropped: Route ${routeKey} is suspended`,
|
|
175
|
-
);
|
|
176
|
-
resolve(null);
|
|
177
|
-
continue;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// Check rate limit before processing - suspend immediately if exhausted
|
|
87
|
+
// Wait for the known bucket reset instead of dropping the request.
|
|
181
88
|
const rateLimit = this.rateLimits.get(routeKey);
|
|
182
89
|
if (
|
|
183
90
|
rateLimit &&
|
|
184
91
|
rateLimit.remaining <= 0 &&
|
|
185
92
|
Date.now() < rateLimit.reset
|
|
186
93
|
) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
this.
|
|
190
|
-
|
|
191
|
-
reason: "Rate limit exhausted during queue processing",
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
console.log(
|
|
195
|
-
`⏸️ Suspending route ${routeKey} for 2 minutes (queue processing)`,
|
|
196
|
-
);
|
|
197
|
-
|
|
198
|
-
// Clear remaining queued requests for this route
|
|
199
|
-
const clearedCount = this.clearQueuedRequestsForRoute(routeKey);
|
|
200
|
-
if (clearedCount > 0) {
|
|
201
|
-
console.log(
|
|
202
|
-
`🗑️ Cleared ${clearedCount} additional queued requests for ${routeKey}`,
|
|
203
|
-
);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
resolve(null);
|
|
207
|
-
continue;
|
|
94
|
+
const delay = rateLimit.reset - Date.now();
|
|
95
|
+
console.log(`⏳ Route ${routeKey} rate limited, waiting ${delay}ms`);
|
|
96
|
+
await this.sleep(delay);
|
|
97
|
+
this.rateLimits.delete(routeKey);
|
|
208
98
|
}
|
|
209
99
|
|
|
210
100
|
const result = await this.makeRequest(endpoint, options);
|
|
211
101
|
resolve(result);
|
|
212
|
-
|
|
213
|
-
// Delay between requests to be conservative with rate limits
|
|
214
|
-
await this.sleep(500);
|
|
215
102
|
} catch (error) {
|
|
216
103
|
reject(error);
|
|
217
104
|
}
|
|
218
105
|
}
|
|
219
106
|
|
|
220
|
-
this.
|
|
107
|
+
this.processingRoutes.delete(routeKey);
|
|
108
|
+
this.requestQueues.delete(routeKey);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Wait for a shared global rate limit to expire
|
|
113
|
+
* @private
|
|
114
|
+
*/
|
|
115
|
+
async waitForGlobalRateLimit() {
|
|
116
|
+
while (this.globalRateLimit && Date.now() < this.globalRateLimit.reset) {
|
|
117
|
+
const delay = this.globalRateLimit.reset - Date.now();
|
|
118
|
+
console.log(`⏳ Global rate limit active, waiting ${delay}ms`);
|
|
119
|
+
|
|
120
|
+
if (!this.globalDelay) {
|
|
121
|
+
this.globalDelay = this.sleep(delay).finally(() => {
|
|
122
|
+
this.globalDelay = null;
|
|
123
|
+
if (
|
|
124
|
+
this.globalRateLimit &&
|
|
125
|
+
Date.now() >= this.globalRateLimit.reset
|
|
126
|
+
) {
|
|
127
|
+
this.globalRateLimit = null;
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await this.globalDelay;
|
|
133
|
+
}
|
|
221
134
|
}
|
|
222
135
|
|
|
223
136
|
/**
|
|
@@ -254,10 +167,25 @@ class RestManager {
|
|
|
254
167
|
|
|
255
168
|
// Handle rate limiting
|
|
256
169
|
if (response.status === 429) {
|
|
257
|
-
const
|
|
258
|
-
|
|
170
|
+
const error = await response.json().catch(() => ({}));
|
|
171
|
+
const bodyRetryAfter = Number(error.retry_after);
|
|
172
|
+
const resetAfter = parseFloat(
|
|
173
|
+
response.headers.get("x-ratelimit-reset-after"),
|
|
174
|
+
);
|
|
175
|
+
const headerRetryAfter = parseFloat(
|
|
176
|
+
response.headers.get("retry-after"),
|
|
177
|
+
);
|
|
178
|
+
const retryAfterSeconds = Number.isFinite(bodyRetryAfter)
|
|
179
|
+
? bodyRetryAfter
|
|
180
|
+
: Number.isFinite(resetAfter)
|
|
181
|
+
? resetAfter
|
|
182
|
+
: headerRetryAfter;
|
|
183
|
+
const retryAfter = Number.isFinite(retryAfterSeconds)
|
|
184
|
+
? Math.max(0, retryAfterSeconds * 1000)
|
|
185
|
+
: 2000;
|
|
259
186
|
const isGlobal =
|
|
260
|
-
response.headers.get("x-ratelimit-global") === "true"
|
|
187
|
+
response.headers.get("x-ratelimit-global") === "true" ||
|
|
188
|
+
error.global === true;
|
|
261
189
|
const routeKey = this.getRouteKey(endpoint, options.method || "GET");
|
|
262
190
|
|
|
263
191
|
console.log(
|
|
@@ -267,66 +195,24 @@ class RestManager {
|
|
|
267
195
|
);
|
|
268
196
|
|
|
269
197
|
if (isGlobal) {
|
|
270
|
-
this.globalRateLimit = { reset: Date.now() + retryAfter
|
|
271
|
-
console.log(
|
|
272
|
-
`⏳ Global rate limit set, waiting ${retryAfter + 2000}ms`,
|
|
273
|
-
);
|
|
198
|
+
this.globalRateLimit = { reset: Date.now() + retryAfter };
|
|
199
|
+
console.log(`⏳ Global rate limit set, waiting ${retryAfter}ms`);
|
|
274
200
|
} else {
|
|
275
|
-
const suspensionTime = retryAfter + 500;
|
|
276
|
-
this.suspendedRoutes.set(routeKey, {
|
|
277
|
-
suspendedUntil: Date.now() + suspensionTime,
|
|
278
|
-
reason: `429 response on attempt ${attempt + 1}`,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
console.log(
|
|
282
|
-
`⏸️ Suspending route ${routeKey} for ${suspensionTime}ms (429 response)`,
|
|
283
|
-
);
|
|
284
|
-
|
|
285
|
-
// Clear queue for this route
|
|
286
|
-
const clearedCount = this.clearQueuedRequestsForRoute(routeKey);
|
|
287
|
-
if (clearedCount > 0) {
|
|
288
|
-
console.log(
|
|
289
|
-
`�️ Cleared ${clearedCount} queued requests for ${routeKey}`,
|
|
290
|
-
);
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
// Update rate limit state
|
|
294
201
|
this.rateLimits.set(routeKey, {
|
|
295
202
|
remaining: 0,
|
|
296
|
-
reset: Date.now() +
|
|
203
|
+
reset: Date.now() + retryAfter,
|
|
297
204
|
limit: this.rateLimits.get(routeKey)?.limit || 5,
|
|
298
205
|
updatedAt: Date.now(),
|
|
299
206
|
});
|
|
300
|
-
|
|
301
|
-
// Open circuit breaker after 2 consecutive 429s
|
|
302
|
-
const breaker = this.circuitBreakers.get(routeKey) || {
|
|
303
|
-
failures: 0,
|
|
304
|
-
lastFailure: 0,
|
|
305
|
-
openUntil: 0,
|
|
306
|
-
};
|
|
307
|
-
breaker.failures++;
|
|
308
|
-
breaker.lastFailure = Date.now();
|
|
309
|
-
|
|
310
|
-
if (breaker.failures >= 2) {
|
|
311
|
-
breaker.openUntil = Date.now() + suspensionTime;
|
|
312
|
-
console.log(`🔌 Circuit breaker OPENED for ${routeKey}`);
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
this.circuitBreakers.set(routeKey, breaker);
|
|
316
|
-
|
|
317
|
-
// Don't retry, return null immediately
|
|
318
|
-
return null;
|
|
319
207
|
}
|
|
320
208
|
|
|
321
|
-
|
|
209
|
+
console.log(`⏳ Retrying ${routeKey} in ${retryAfter}ms`);
|
|
322
210
|
if (isGlobal) {
|
|
323
|
-
await this.
|
|
324
|
-
|
|
325
|
-
|
|
211
|
+
await this.waitForGlobalRateLimit();
|
|
212
|
+
} else {
|
|
213
|
+
await this.sleep(retryAfter);
|
|
326
214
|
}
|
|
327
|
-
|
|
328
|
-
// For route rate limits, don't retry
|
|
329
|
-
return null;
|
|
215
|
+
continue;
|
|
330
216
|
}
|
|
331
217
|
|
|
332
218
|
if (!response.ok) {
|
|
@@ -343,18 +229,9 @@ class RestManager {
|
|
|
343
229
|
|
|
344
230
|
// Handle 204 No Content (successful but no body)
|
|
345
231
|
if (response.status === 204) {
|
|
346
|
-
// Reset circuit breaker and suspension on successful response
|
|
347
|
-
const routeKey = this.getRouteKey(endpoint, options.method || "GET");
|
|
348
|
-
this.circuitBreakers.delete(routeKey);
|
|
349
|
-
this.suspendedRoutes.delete(routeKey);
|
|
350
232
|
return null;
|
|
351
233
|
}
|
|
352
234
|
|
|
353
|
-
// Reset circuit breaker and suspension on successful response
|
|
354
|
-
const routeKey = this.getRouteKey(endpoint, options.method || "GET");
|
|
355
|
-
this.circuitBreakers.delete(routeKey);
|
|
356
|
-
this.suspendedRoutes.delete(routeKey);
|
|
357
|
-
|
|
358
235
|
return await response.json();
|
|
359
236
|
} catch (error) {
|
|
360
237
|
// Do not retry on Missing Permissions (50013)
|
|
@@ -392,12 +269,20 @@ class RestManager {
|
|
|
392
269
|
|
|
393
270
|
const remaining = parseInt(headers.get("x-ratelimit-remaining"));
|
|
394
271
|
const resetTimestamp = parseFloat(headers.get("x-ratelimit-reset"));
|
|
272
|
+
const resetAfter = parseFloat(headers.get("x-ratelimit-reset-after"));
|
|
395
273
|
const limit = parseInt(headers.get("x-ratelimit-limit"));
|
|
396
274
|
|
|
397
|
-
if (!isNaN(remaining) && !isNaN(resetTimestamp)) {
|
|
275
|
+
if (!isNaN(remaining) && (!isNaN(resetAfter) || !isNaN(resetTimestamp))) {
|
|
276
|
+
const serverTime = Date.parse(headers.get("date"));
|
|
277
|
+
const clockOffset = Number.isNaN(serverTime)
|
|
278
|
+
? 0
|
|
279
|
+
: serverTime - Date.now();
|
|
280
|
+
|
|
398
281
|
this.rateLimits.set(routeKey, {
|
|
399
282
|
remaining,
|
|
400
|
-
reset:
|
|
283
|
+
reset: !isNaN(resetAfter)
|
|
284
|
+
? Date.now() + resetAfter * 1000
|
|
285
|
+
: resetTimestamp * 1000 - clockOffset,
|
|
401
286
|
limit,
|
|
402
287
|
updatedAt: Date.now(),
|
|
403
288
|
});
|
|
@@ -412,16 +297,21 @@ class RestManager {
|
|
|
412
297
|
* @private
|
|
413
298
|
*/
|
|
414
299
|
getRouteKey(endpoint, method) {
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
const normalized =
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
300
|
+
const majorParameters = new Set(["channels", "guilds"]);
|
|
301
|
+
const segments = endpoint.split("?", 1)[0].split("/");
|
|
302
|
+
const normalized = [];
|
|
303
|
+
|
|
304
|
+
for (let index = 0; index < segments.length; index++) {
|
|
305
|
+
if (segments[index - 1] === "reactions") break;
|
|
306
|
+
|
|
307
|
+
const segment = segments[index];
|
|
308
|
+
const isMinorId =
|
|
309
|
+
/^\d{16,19}$/.test(segment) &&
|
|
310
|
+
!majorParameters.has(segments[index - 1]);
|
|
311
|
+
normalized.push(isMinorId ? "{id}" : segment);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
return `${method.toUpperCase()}:${normalized.join("/")}`;
|
|
425
315
|
}
|
|
426
316
|
|
|
427
317
|
/**
|
|
@@ -435,20 +325,6 @@ class RestManager {
|
|
|
435
325
|
this.rateLimits.delete(key);
|
|
436
326
|
}
|
|
437
327
|
}
|
|
438
|
-
|
|
439
|
-
// Clean up expired circuit breakers
|
|
440
|
-
for (const [key, breaker] of this.circuitBreakers.entries()) {
|
|
441
|
-
if (now > breaker.openUntil) {
|
|
442
|
-
this.circuitBreakers.delete(key);
|
|
443
|
-
}
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Clean up expired route suspensions
|
|
447
|
-
for (const [key, suspension] of this.suspendedRoutes.entries()) {
|
|
448
|
-
if (now > suspension.suspendedUntil) {
|
|
449
|
-
this.suspendedRoutes.delete(key);
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
328
|
}
|
|
453
329
|
|
|
454
330
|
/**
|
|
@@ -461,24 +337,6 @@ class RestManager {
|
|
|
461
337
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
462
338
|
}
|
|
463
339
|
|
|
464
|
-
/**
|
|
465
|
-
* Clear queued requests for a specific route
|
|
466
|
-
* @private
|
|
467
|
-
* @param {string} routeKey - The route key to clear requests for
|
|
468
|
-
* @returns {number} Number of requests cleared
|
|
469
|
-
*/
|
|
470
|
-
clearQueuedRequestsForRoute(routeKey) {
|
|
471
|
-
const initialLength = this.requestQueue.length;
|
|
472
|
-
this.requestQueue = this.requestQueue.filter((request) => {
|
|
473
|
-
const requestRouteKey = this.getRouteKey(
|
|
474
|
-
request.endpoint,
|
|
475
|
-
request.options.method || "GET",
|
|
476
|
-
);
|
|
477
|
-
return requestRouteKey !== routeKey;
|
|
478
|
-
});
|
|
479
|
-
return initialLength - this.requestQueue.length;
|
|
480
|
-
}
|
|
481
|
-
|
|
482
340
|
/**
|
|
483
341
|
* Check if a route is currently rate limited
|
|
484
342
|
* @param {string} endpoint - The API endpoint
|
|
@@ -509,6 +367,11 @@ class RestManager {
|
|
|
509
367
|
getRateLimitStatus(endpoint, method = "GET") {
|
|
510
368
|
const routeKey = this.getRouteKey(endpoint, method);
|
|
511
369
|
const rateLimit = this.rateLimits.get(routeKey);
|
|
370
|
+
const routeQueueLength = this.requestQueues.get(routeKey)?.length || 0;
|
|
371
|
+
const queueLength = Array.from(this.requestQueues.values()).reduce(
|
|
372
|
+
(total, queue) => total + queue.length,
|
|
373
|
+
0,
|
|
374
|
+
);
|
|
512
375
|
|
|
513
376
|
return {
|
|
514
377
|
routeKey,
|
|
@@ -529,8 +392,10 @@ class RestManager {
|
|
|
529
392
|
resetIn: Math.max(0, rateLimit.reset - Date.now()),
|
|
530
393
|
}
|
|
531
394
|
: null,
|
|
532
|
-
queueLength
|
|
533
|
-
|
|
395
|
+
queueLength,
|
|
396
|
+
routeQueueLength,
|
|
397
|
+
isProcessing: this.processingRoutes.size > 0,
|
|
398
|
+
isRouteProcessing: this.processingRoutes.has(routeKey),
|
|
534
399
|
};
|
|
535
400
|
}
|
|
536
401
|
|
|
@@ -619,6 +484,18 @@ class RestManager {
|
|
|
619
484
|
return updatedData;
|
|
620
485
|
}
|
|
621
486
|
|
|
487
|
+
/**
|
|
488
|
+
* Delete a message from a channel
|
|
489
|
+
* @param {string} channelId - The channel ID
|
|
490
|
+
* @param {string} messageId - The message ID
|
|
491
|
+
* @returns {Promise<void>}
|
|
492
|
+
*/
|
|
493
|
+
async deleteMessage(channelId, messageId) {
|
|
494
|
+
await this.request(`/channels/${channelId}/messages/${messageId}`, {
|
|
495
|
+
method: "DELETE",
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
622
499
|
/**
|
|
623
500
|
* React to a message
|
|
624
501
|
* @param {string} channelId - The channel ID
|
|
@@ -349,6 +349,12 @@ class WebhookClient {
|
|
|
349
349
|
if (options.threadId) {
|
|
350
350
|
payload.thread_id = options.threadId;
|
|
351
351
|
}
|
|
352
|
+
if (options.attachments !== undefined) {
|
|
353
|
+
payload.attachments = options.attachments;
|
|
354
|
+
}
|
|
355
|
+
if (options.files !== undefined) {
|
|
356
|
+
payload.files = options.files;
|
|
357
|
+
}
|
|
352
358
|
|
|
353
359
|
return payload;
|
|
354
360
|
}
|