lunibee 0.1.7 → 0.2.0
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/LICENSE +21 -21
- package/README.md +225 -223
- package/dist/builders/commands.d.ts +6 -4
- package/dist/builders/components.d.ts +23 -0
- package/dist/builders/index.d.ts +2 -2
- package/dist/builders/index.js +120 -24
- package/dist/builders/index.js.map +9 -8
- package/dist/collection/index.d.ts +68 -1
- package/dist/collection/index.js +335 -1
- package/dist/collection/index.js.map +4 -4
- package/dist/core/collector.d.ts +13 -0
- package/dist/core/events.d.ts +112 -2
- package/dist/core/index.d.ts +30 -93
- package/dist/core/index.js +3386 -957
- package/dist/core/index.js.map +48 -24
- package/dist/core/permissions.d.ts +48 -0
- package/dist/formatters/index.js.map +1 -1
- package/dist/handlers/index.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3806 -1193
- package/dist/index.js.map +64 -43
- package/dist/managers/advanced.d.ts +116 -0
- package/dist/managers/base.d.ts +13 -2
- package/dist/managers/emoji.d.ts +1 -1
- package/dist/managers/guild-resources.d.ts +109 -0
- package/dist/managers/guild.d.ts +33 -2
- package/dist/managers/index.d.ts +44 -6
- package/dist/managers/index.js +1186 -150
- package/dist/managers/index.js.map +27 -21
- package/dist/managers/message.d.ts +10 -1
- package/dist/rest/decoder.d.ts +23 -0
- package/dist/rest/errors.d.ts +53 -0
- package/dist/rest/index.d.ts +21 -17
- package/dist/rest/index.js +526 -264
- package/dist/rest/index.js.map +14 -8
- package/dist/rest/limiter.d.ts +42 -0
- package/dist/rest/redis.d.ts +18 -1
- package/dist/rest/route.d.ts +31 -0
- package/dist/rest/routes.d.ts +22 -0
- package/dist/rest/scheduler.d.ts +42 -0
- package/dist/rest/store.d.ts +29 -0
- package/dist/rest/transport.d.ts +40 -0
- package/dist/sharding/bus.d.ts +31 -0
- package/dist/sharding/cluster.d.ts +10 -0
- package/dist/sharding/index.d.ts +39 -2
- package/dist/sharding/index.js +1105 -404
- package/dist/sharding/index.js.map +18 -8
- package/dist/structures/base.d.ts +10 -0
- package/dist/structures/channels.d.ts +56 -0
- package/dist/structures/index.d.ts +1 -0
- package/dist/structures/index.js +305 -311
- package/dist/structures/index.js.map +18 -14
- package/dist/structures/interactions.d.ts +38 -52
- package/dist/structures/options.d.ts +55 -0
- package/dist/structures/resources.d.ts +3 -3
- package/dist/types/gateway-events.d.ts +132 -0
- package/dist/types/gateway.d.ts +197 -0
- package/dist/types/index.d.ts +53 -260
- package/dist/types/index.js +44 -2
- package/dist/types/index.js.map +5 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/voice/index.d.ts +32 -1
- package/dist/voice/index.js +73 -7
- package/dist/voice/index.js.map +3 -3
- package/dist/ws/close-codes.d.ts +23 -0
- package/dist/ws/decoder.d.ts +58 -0
- package/dist/ws/heartbeat.d.ts +87 -0
- package/dist/ws/index.d.ts +18 -65
- package/dist/ws/index.js +911 -385
- package/dist/ws/index.js.map +15 -5
- package/dist/ws/opcodes.d.ts +14 -0
- package/dist/ws/protocol.d.ts +110 -0
- package/dist/ws/reconnect.d.ts +108 -0
- package/dist/ws/send-budget.d.ts +17 -0
- package/dist/ws/session.d.ts +100 -0
- package/dist/ws/state.d.ts +27 -0
- package/dist/ws/transport.d.ts +80 -0
- package/package.json +21 -20
package/dist/rest/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// packages/rest/package.json
|
|
3
3
|
var package_default = {
|
|
4
4
|
name: "@lunibee/rest",
|
|
5
|
-
version: "0.
|
|
5
|
+
version: "0.2.0",
|
|
6
6
|
type: "module",
|
|
7
7
|
module: "./src/index.ts",
|
|
8
8
|
types: "./src/index.ts",
|
|
@@ -27,6 +27,17 @@ class MemoryRateLimitStore {
|
|
|
27
27
|
getBucket(key) {
|
|
28
28
|
return this.#buckets.get(key);
|
|
29
29
|
}
|
|
30
|
+
reserve(key) {
|
|
31
|
+
const state = this.#buckets.get(key);
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (!state || state.resetAt <= now)
|
|
34
|
+
return { granted: true };
|
|
35
|
+
if (state.remaining > 0) {
|
|
36
|
+
state.remaining -= 1;
|
|
37
|
+
return { granted: true };
|
|
38
|
+
}
|
|
39
|
+
return { granted: false, retryAfterMs: state.resetAt - now };
|
|
40
|
+
}
|
|
30
41
|
updateBucket(key, state) {
|
|
31
42
|
this.#buckets.set(key, state);
|
|
32
43
|
const now = Date.now();
|
|
@@ -50,6 +61,327 @@ class MemoryRateLimitStore {
|
|
|
50
61
|
}
|
|
51
62
|
}
|
|
52
63
|
|
|
64
|
+
// packages/rest/src/errors.ts
|
|
65
|
+
function kindFromStatus(status) {
|
|
66
|
+
if (status === 429)
|
|
67
|
+
return "rateLimited";
|
|
68
|
+
if (status === 401)
|
|
69
|
+
return "authentication";
|
|
70
|
+
if (status === 403)
|
|
71
|
+
return "permission";
|
|
72
|
+
if (status === 404)
|
|
73
|
+
return "notFound";
|
|
74
|
+
if (status === 400 || status === 422)
|
|
75
|
+
return "validation";
|
|
76
|
+
if (status >= 500)
|
|
77
|
+
return "server";
|
|
78
|
+
if (status === 0)
|
|
79
|
+
return "network";
|
|
80
|
+
return "client";
|
|
81
|
+
}
|
|
82
|
+
function redactPath(path) {
|
|
83
|
+
return path.replace(/^(\/webhooks\/[^/?]+\/)[^/?]+/, "$1:token").replace(/^(\/interactions\/[^/?]+\/)[^/?]+/, "$1:token");
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
class RESTError extends Error {
|
|
87
|
+
kind;
|
|
88
|
+
status;
|
|
89
|
+
code;
|
|
90
|
+
errors;
|
|
91
|
+
method;
|
|
92
|
+
path;
|
|
93
|
+
constructor(message, status, code, errors, options = {}) {
|
|
94
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
95
|
+
this.name = "RESTError";
|
|
96
|
+
this.status = status;
|
|
97
|
+
this.kind = options.kind ?? kindFromStatus(status);
|
|
98
|
+
this.code = code;
|
|
99
|
+
this.errors = errors;
|
|
100
|
+
this.method = options.method;
|
|
101
|
+
this.path = options.path === undefined ? undefined : redactPath(options.path);
|
|
102
|
+
}
|
|
103
|
+
get retryable() {
|
|
104
|
+
return this.kind === "rateLimited" || this.kind === "server" || this.kind === "network" || this.kind === "timeout";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function abortError(path, reason) {
|
|
108
|
+
return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason, kind: "aborted" });
|
|
109
|
+
}
|
|
110
|
+
async function sleep(delay, signal, path) {
|
|
111
|
+
if (delay <= 0)
|
|
112
|
+
return;
|
|
113
|
+
await new Promise((resolve, reject) => {
|
|
114
|
+
const timer = setTimeout(resolve, delay);
|
|
115
|
+
const abort = () => {
|
|
116
|
+
clearTimeout(timer);
|
|
117
|
+
reject(abortError(path, signal?.reason));
|
|
118
|
+
};
|
|
119
|
+
if (signal?.aborted)
|
|
120
|
+
return abort();
|
|
121
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
async function abortable(promise, signal, path) {
|
|
125
|
+
if (!signal)
|
|
126
|
+
return promise;
|
|
127
|
+
if (signal.aborted)
|
|
128
|
+
throw abortError(path, signal.reason);
|
|
129
|
+
return Promise.race([
|
|
130
|
+
promise,
|
|
131
|
+
new Promise((_, reject) => signal.addEventListener("abort", () => reject(abortError(path, signal.reason)), { once: true }))
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// packages/rest/src/route.ts
|
|
136
|
+
function normalizeRoutePath(path) {
|
|
137
|
+
const queryIndex = path.indexOf("?");
|
|
138
|
+
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
139
|
+
return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
140
|
+
}
|
|
141
|
+
function majorParameter(path) {
|
|
142
|
+
const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/.exec(path);
|
|
143
|
+
if (!match)
|
|
144
|
+
return "@none";
|
|
145
|
+
if (match[1] === "webhooks" && match[3] !== undefined)
|
|
146
|
+
return `${match[2]}:${match[3]}`;
|
|
147
|
+
return match[2];
|
|
148
|
+
}
|
|
149
|
+
function scopeBucket(hashOrRoute, major) {
|
|
150
|
+
return `${hashOrRoute}|${major}`;
|
|
151
|
+
}
|
|
152
|
+
function createRouteKey(method, path) {
|
|
153
|
+
const normalizedMethod = method.toUpperCase();
|
|
154
|
+
return {
|
|
155
|
+
method: normalizedMethod,
|
|
156
|
+
path,
|
|
157
|
+
route: `${normalizedMethod}:${normalizeRoutePath(path)}`,
|
|
158
|
+
major: majorParameter(path)
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function serializeQuery(query) {
|
|
162
|
+
if (query === undefined)
|
|
163
|
+
return "";
|
|
164
|
+
if (typeof query === "string")
|
|
165
|
+
return query.length === 0 || query.startsWith("?") ? query : `?${query}`;
|
|
166
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => value === undefined || value === null ? [] : [[key, String(value)]]));
|
|
167
|
+
const serialized = params.toString();
|
|
168
|
+
return serialized.length === 0 ? "" : `?${serialized}`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// packages/rest/src/scheduler.ts
|
|
172
|
+
class RequestScheduler {
|
|
173
|
+
static MAX_REMAPS = 2;
|
|
174
|
+
#queues = new Map;
|
|
175
|
+
get size() {
|
|
176
|
+
return this.#queues.size;
|
|
177
|
+
}
|
|
178
|
+
async runResolved(resolveKey, signal, path, task) {
|
|
179
|
+
let key = await resolveKey();
|
|
180
|
+
for (let hop = 0;hop < RequestScheduler.MAX_REMAPS; hop++) {
|
|
181
|
+
const outcome = await this.run(key, signal, path, async (release) => {
|
|
182
|
+
const current = await resolveKey();
|
|
183
|
+
if (current !== key)
|
|
184
|
+
return { done: false, key: current };
|
|
185
|
+
return { done: true, value: await task(key, release) };
|
|
186
|
+
});
|
|
187
|
+
if (outcome.done)
|
|
188
|
+
return outcome.value;
|
|
189
|
+
key = outcome.key;
|
|
190
|
+
}
|
|
191
|
+
return this.run(key, signal, path, (release) => task(key, release));
|
|
192
|
+
}
|
|
193
|
+
async run(key, signal, path, task) {
|
|
194
|
+
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
195
|
+
let open;
|
|
196
|
+
const gate = new Promise((resolve) => {
|
|
197
|
+
open = resolve;
|
|
198
|
+
});
|
|
199
|
+
this.#queues.set(key, gate);
|
|
200
|
+
let released = false;
|
|
201
|
+
const release = () => {
|
|
202
|
+
if (released)
|
|
203
|
+
return;
|
|
204
|
+
released = true;
|
|
205
|
+
open();
|
|
206
|
+
if (this.#queues.get(key) === gate)
|
|
207
|
+
this.#queues.delete(key);
|
|
208
|
+
};
|
|
209
|
+
try {
|
|
210
|
+
await abortable(previous, signal, path);
|
|
211
|
+
return await task(release);
|
|
212
|
+
} finally {
|
|
213
|
+
release();
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// packages/rest/src/limiter.ts
|
|
219
|
+
class RateLimiter {
|
|
220
|
+
#store;
|
|
221
|
+
#concurrent;
|
|
222
|
+
constructor(store, options = {}) {
|
|
223
|
+
this.#store = store;
|
|
224
|
+
this.#concurrent = options.concurrent ?? false;
|
|
225
|
+
}
|
|
226
|
+
get store() {
|
|
227
|
+
return this.#store;
|
|
228
|
+
}
|
|
229
|
+
async resolveBucketKey(route, major) {
|
|
230
|
+
return scopeBucket(await this.#store.getBucketHash(route) ?? route, major);
|
|
231
|
+
}
|
|
232
|
+
get reserves() {
|
|
233
|
+
return typeof this.#store.reserve === "function";
|
|
234
|
+
}
|
|
235
|
+
async acquire(bucketKey, signal, path) {
|
|
236
|
+
await this.#waitGlobal(signal, path);
|
|
237
|
+
if (!this.#store.reserve) {
|
|
238
|
+
const bucket = await this.#store.getBucket(bucketKey);
|
|
239
|
+
if (bucket) {
|
|
240
|
+
const delay = bucket.resetAt - Date.now();
|
|
241
|
+
if (bucket.remaining <= 0 && delay > 0)
|
|
242
|
+
await sleep(delay, signal, path);
|
|
243
|
+
}
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
for (let attempt = 0;attempt < 8; attempt++) {
|
|
247
|
+
const reservation = await this.#store.reserve(bucketKey);
|
|
248
|
+
if (reservation.granted)
|
|
249
|
+
return;
|
|
250
|
+
await sleep(Math.max(1, reservation.retryAfterMs), signal, path);
|
|
251
|
+
await this.#waitGlobal(signal, path);
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
async#waitGlobal(signal, path) {
|
|
255
|
+
const globalDelay = await this.#store.getGlobalReset() - Date.now();
|
|
256
|
+
if (globalDelay > 0)
|
|
257
|
+
await sleep(globalDelay, signal, path);
|
|
258
|
+
}
|
|
259
|
+
async noteGlobalReset(resetAt) {
|
|
260
|
+
const current = await this.#store.getGlobalReset();
|
|
261
|
+
await this.#store.setGlobalReset(Math.max(current, resetAt));
|
|
262
|
+
}
|
|
263
|
+
async applyResponse(response, bucketKey, route, major) {
|
|
264
|
+
let currentKey = bucketKey;
|
|
265
|
+
const serverBucket = response.headers.get("X-RateLimit-Bucket");
|
|
266
|
+
if (serverBucket) {
|
|
267
|
+
await this.#store.setBucketHash(route, serverBucket);
|
|
268
|
+
currentKey = scopeBucket(serverBucket, major);
|
|
269
|
+
}
|
|
270
|
+
const stored = await this.#store.getBucket(currentKey);
|
|
271
|
+
const bucket = stored ? { ...stored } : { remaining: 1, resetAt: 0 };
|
|
272
|
+
const windowActive = this.#concurrent && !!stored && stored.resetAt > Date.now();
|
|
273
|
+
const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
|
|
274
|
+
const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
|
|
275
|
+
const reset = Number(response.headers.get("X-RateLimit-Reset"));
|
|
276
|
+
if (Number.isFinite(remaining))
|
|
277
|
+
bucket.remaining = Math.max(0, remaining);
|
|
278
|
+
if (Number.isFinite(resetAfter))
|
|
279
|
+
bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
|
|
280
|
+
else if (Number.isFinite(reset))
|
|
281
|
+
bucket.resetAt = reset * 1000;
|
|
282
|
+
if (windowActive && stored) {
|
|
283
|
+
bucket.remaining = Math.min(bucket.remaining, stored.remaining);
|
|
284
|
+
bucket.resetAt = Math.max(bucket.resetAt, stored.resetAt);
|
|
285
|
+
}
|
|
286
|
+
if (response.status === 429) {
|
|
287
|
+
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
288
|
+
if (Number.isFinite(retryAfter))
|
|
289
|
+
bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
|
|
290
|
+
}
|
|
291
|
+
await this.#store.updateBucket(currentKey, bucket);
|
|
292
|
+
if (response.headers.get("X-RateLimit-Global") === "true") {
|
|
293
|
+
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
294
|
+
if (Number.isFinite(retryAfter))
|
|
295
|
+
await this.noteGlobalReset(Date.now() + retryAfter * 1000);
|
|
296
|
+
}
|
|
297
|
+
return currentKey;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// packages/rest/src/transport.ts
|
|
302
|
+
class TransportError extends Error {
|
|
303
|
+
timedOut;
|
|
304
|
+
constructor(message, timedOut, cause) {
|
|
305
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
306
|
+
this.name = "TransportError";
|
|
307
|
+
this.timedOut = timedOut;
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
class HttpTransport {
|
|
312
|
+
#baseURL;
|
|
313
|
+
#timeout;
|
|
314
|
+
#fetch;
|
|
315
|
+
constructor(options = {}) {
|
|
316
|
+
this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
|
|
317
|
+
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
318
|
+
this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
319
|
+
}
|
|
320
|
+
get baseURL() {
|
|
321
|
+
return this.#baseURL;
|
|
322
|
+
}
|
|
323
|
+
get timeout() {
|
|
324
|
+
return this.#timeout;
|
|
325
|
+
}
|
|
326
|
+
async send(request) {
|
|
327
|
+
if (request.signal?.aborted)
|
|
328
|
+
throw abortError(request.path, request.signal.reason);
|
|
329
|
+
const controller = new AbortController;
|
|
330
|
+
const onAbort = () => controller.abort(request.signal?.reason);
|
|
331
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
332
|
+
const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
|
|
333
|
+
try {
|
|
334
|
+
return await this.#fetch(`${this.#baseURL}${request.path}`, {
|
|
335
|
+
method: request.method,
|
|
336
|
+
headers: request.headers,
|
|
337
|
+
body: request.body,
|
|
338
|
+
signal: controller.signal
|
|
339
|
+
});
|
|
340
|
+
} catch (error) {
|
|
341
|
+
if (error instanceof RESTError)
|
|
342
|
+
throw error;
|
|
343
|
+
const timedOut = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
344
|
+
throw new TransportError(timedOut ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", timedOut, error);
|
|
345
|
+
} finally {
|
|
346
|
+
clearTimeout(timer);
|
|
347
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// packages/rest/src/decoder.ts
|
|
353
|
+
class ResponseDecoder {
|
|
354
|
+
async read(response) {
|
|
355
|
+
if (response.status === 204)
|
|
356
|
+
return;
|
|
357
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
358
|
+
if (contentType.includes("application/json"))
|
|
359
|
+
return response.json().catch(() => {
|
|
360
|
+
return;
|
|
361
|
+
});
|
|
362
|
+
return response.text().catch(() => {
|
|
363
|
+
return;
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
errorData(payload) {
|
|
367
|
+
if (!payload || typeof payload !== "object")
|
|
368
|
+
return {};
|
|
369
|
+
const data = payload;
|
|
370
|
+
return {
|
|
371
|
+
message: typeof data.message === "string" ? data.message : undefined,
|
|
372
|
+
code: typeof data.code === "number" ? data.code : undefined,
|
|
373
|
+
retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
|
|
374
|
+
global: data.global === true
|
|
375
|
+
};
|
|
376
|
+
}
|
|
377
|
+
retryAfter(response, data) {
|
|
378
|
+
const raw = response.headers.get("Retry-After");
|
|
379
|
+
const header = raw === null ? Number.NaN : Number(raw);
|
|
380
|
+
const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
|
|
381
|
+
return Math.max(0, retryAfter);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
53
385
|
// packages/rest/src/routes.ts
|
|
54
386
|
var Routes = {
|
|
55
387
|
user: () => "/users/@me",
|
|
@@ -91,6 +423,8 @@ var Routes = {
|
|
|
91
423
|
threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
|
|
92
424
|
guildScheduledEvents: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events`,
|
|
93
425
|
guildScheduledEvent: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}`,
|
|
426
|
+
guildScheduledEventUsers: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}/users`,
|
|
427
|
+
channelPermission: (channelId, overwriteId) => `/channels/${snowflake(channelId, "Channel ID")}/permissions/${snowflake(overwriteId, "Overwrite ID")}`,
|
|
94
428
|
guildAutoModerationRules: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules`,
|
|
95
429
|
guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
|
|
96
430
|
voiceRegions: () => "/voice/regions",
|
|
@@ -106,23 +440,59 @@ var Routes = {
|
|
|
106
440
|
invite: (code) => `/invites/${encodeURIComponent(code)}`,
|
|
107
441
|
guildVanity: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/vanity-url`,
|
|
108
442
|
guildEmoji: (guildId, emojiId) => emojiId ? `/guilds/${snowflake(guildId, "Guild ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/emojis`,
|
|
109
|
-
applicationEmoji: (applicationId, emojiId) => emojiId ? `/applications/${snowflake(applicationId, "Application ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/emojis
|
|
443
|
+
applicationEmoji: (applicationId, emojiId) => emojiId ? `/applications/${snowflake(applicationId, "Application ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/emojis`,
|
|
444
|
+
pollExpire: (channelId, messageId) => `/channels/${snowflake(channelId, "Channel ID")}/polls/${snowflake(messageId, "Message ID")}/expire`,
|
|
445
|
+
pollAnswerVoters: (channelId, messageId, answerId) => `/channels/${snowflake(channelId, "Channel ID")}/polls/${snowflake(messageId, "Message ID")}/answers/${integer(answerId, "Answer ID")}`,
|
|
446
|
+
guildSticker: (guildId, stickerId) => stickerId ? `/guilds/${snowflake(guildId, "Guild ID")}/stickers/${snowflake(stickerId, "Sticker ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/stickers`,
|
|
447
|
+
sendSoundboardSound: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/send-soundboard-sound`,
|
|
448
|
+
soundboardDefaultSounds: () => "/soundboard-default-sounds",
|
|
449
|
+
guildSoundboardSound: (guildId, soundId) => soundId ? `/guilds/${snowflake(guildId, "Guild ID")}/soundboard-sounds/${snowflake(soundId, "Sound ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/soundboard-sounds`,
|
|
450
|
+
applicationSkus: (applicationId) => `/applications/${snowflake(applicationId, "Application ID")}/skus`,
|
|
451
|
+
applicationEntitlement: (applicationId, entitlementId) => entitlementId ? `/applications/${snowflake(applicationId, "Application ID")}/entitlements/${snowflake(entitlementId, "Entitlement ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/entitlements`,
|
|
452
|
+
consumeEntitlement: (applicationId, entitlementId) => `/applications/${snowflake(applicationId, "Application ID")}/entitlements/${snowflake(entitlementId, "Entitlement ID")}/consume`,
|
|
453
|
+
skuSubscription: (skuId, subscriptionId) => subscriptionId ? `/skus/${snowflake(skuId, "SKU ID")}/subscriptions/${snowflake(subscriptionId, "Subscription ID")}` : `/skus/${snowflake(skuId, "SKU ID")}/subscriptions`
|
|
110
454
|
};
|
|
455
|
+
function integer(value, field) {
|
|
456
|
+
if (!Number.isInteger(value) || value < 0)
|
|
457
|
+
throw new TypeError(`${field} must be a non-negative integer.`);
|
|
458
|
+
return String(value);
|
|
459
|
+
}
|
|
111
460
|
function snowflake(value, field) {
|
|
112
461
|
if (!/^\d{1,20}$/.test(value))
|
|
113
462
|
throw new TypeError(`${field} must be a valid Discord snowflake.`);
|
|
114
463
|
return value;
|
|
115
464
|
}
|
|
116
465
|
// packages/rest/src/redis.ts
|
|
466
|
+
var RESERVE_SCRIPT = `
|
|
467
|
+
local raw = redis.call('GET', KEYS[1])
|
|
468
|
+
if not raw then return {1, 0} end
|
|
469
|
+
local ok, state = pcall(cjson.decode, raw)
|
|
470
|
+
if not ok or type(state) ~= 'table' or type(state.resetAt) ~= 'number' then
|
|
471
|
+
return {1, 0}
|
|
472
|
+
end
|
|
473
|
+
local now = tonumber(ARGV[1])
|
|
474
|
+
local wait = state.resetAt - now
|
|
475
|
+
if wait <= 0 then return {1, 0} end
|
|
476
|
+
if type(state.remaining) == 'number' and state.remaining > 0 then
|
|
477
|
+
state.remaining = state.remaining - 1
|
|
478
|
+
redis.call('SET', KEYS[1], cjson.encode(state), 'PX', math.ceil(wait))
|
|
479
|
+
return {1, 0}
|
|
480
|
+
end
|
|
481
|
+
return {0, math.ceil(wait)}
|
|
482
|
+
`;
|
|
483
|
+
|
|
117
484
|
class RedisRateLimitStore {
|
|
118
485
|
#client;
|
|
119
486
|
#prefix;
|
|
120
487
|
#onErrorCallback;
|
|
488
|
+
#local = new MemoryRateLimitStore;
|
|
121
489
|
#lastError;
|
|
122
490
|
constructor(options) {
|
|
123
491
|
this.#client = options.client;
|
|
124
492
|
this.#prefix = options.prefix ?? "lunibee:rest:";
|
|
125
493
|
this.#onErrorCallback = options.onError;
|
|
494
|
+
if (typeof options.client.eval === "function")
|
|
495
|
+
this.reserve = (key) => this.#reserve(key);
|
|
126
496
|
}
|
|
127
497
|
get lastError() {
|
|
128
498
|
return this.#lastError;
|
|
@@ -147,10 +517,11 @@ class RedisRateLimitStore {
|
|
|
147
517
|
return hash ?? undefined;
|
|
148
518
|
} catch (error) {
|
|
149
519
|
this.#onError("getBucketHash", error);
|
|
150
|
-
return;
|
|
520
|
+
return this.#local.getBucketHash(route);
|
|
151
521
|
}
|
|
152
522
|
}
|
|
153
523
|
async setBucketHash(route, hash) {
|
|
524
|
+
this.#local.setBucketHash(route, hash);
|
|
154
525
|
try {
|
|
155
526
|
await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
|
|
156
527
|
this.#markHealthy();
|
|
@@ -167,10 +538,11 @@ class RedisRateLimitStore {
|
|
|
167
538
|
return JSON.parse(data);
|
|
168
539
|
} catch (error) {
|
|
169
540
|
this.#onError("getBucket", error);
|
|
170
|
-
return;
|
|
541
|
+
return this.#local.getBucket(key);
|
|
171
542
|
}
|
|
172
543
|
}
|
|
173
544
|
async updateBucket(key, state) {
|
|
545
|
+
this.#local.updateBucket(key, state);
|
|
174
546
|
const bucketKey = `${this.#prefix}bucket:${key}`;
|
|
175
547
|
const ttl = Math.ceil((state.resetAt - Date.now()) / 1000);
|
|
176
548
|
try {
|
|
@@ -185,6 +557,23 @@ class RedisRateLimitStore {
|
|
|
185
557
|
this.#onError("updateBucket", error);
|
|
186
558
|
}
|
|
187
559
|
}
|
|
560
|
+
get supportsReservation() {
|
|
561
|
+
return typeof this.#client.eval === "function";
|
|
562
|
+
}
|
|
563
|
+
reserve;
|
|
564
|
+
async#reserve(key) {
|
|
565
|
+
try {
|
|
566
|
+
const result = await this.#client.eval(RESERVE_SCRIPT, 1, `${this.#prefix}bucket:${key}`, Date.now());
|
|
567
|
+
this.#markHealthy();
|
|
568
|
+
if (!Array.isArray(result))
|
|
569
|
+
return { granted: true };
|
|
570
|
+
const [granted, retryAfterMs] = result;
|
|
571
|
+
return Number(granted) === 1 ? { granted: true } : { granted: false, retryAfterMs: Number(retryAfterMs) || 0 };
|
|
572
|
+
} catch (error) {
|
|
573
|
+
this.#onError("reserve", error);
|
|
574
|
+
return this.#local.reserve(key);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
188
577
|
async getGlobalReset() {
|
|
189
578
|
try {
|
|
190
579
|
const resetAt = await this.#client.get(`${this.#prefix}global`);
|
|
@@ -192,10 +581,11 @@ class RedisRateLimitStore {
|
|
|
192
581
|
return resetAt ? Number(resetAt) : 0;
|
|
193
582
|
} catch (error) {
|
|
194
583
|
this.#onError("getGlobalReset", error);
|
|
195
|
-
return
|
|
584
|
+
return this.#local.getGlobalReset();
|
|
196
585
|
}
|
|
197
586
|
}
|
|
198
587
|
async setGlobalReset(resetAt) {
|
|
588
|
+
this.#local.setGlobalReset(resetAt);
|
|
199
589
|
const globalKey = `${this.#prefix}global`;
|
|
200
590
|
const ttl = Math.ceil((resetAt - Date.now()) / 1000);
|
|
201
591
|
try {
|
|
@@ -251,22 +641,6 @@ class WebhookClient {
|
|
|
251
641
|
}
|
|
252
642
|
|
|
253
643
|
// packages/rest/src/index.ts
|
|
254
|
-
class RESTError extends Error {
|
|
255
|
-
status;
|
|
256
|
-
code;
|
|
257
|
-
errors;
|
|
258
|
-
method;
|
|
259
|
-
path;
|
|
260
|
-
constructor(message, status, code, errors, options = {}) {
|
|
261
|
-
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
262
|
-
this.name = "RESTError";
|
|
263
|
-
this.status = status;
|
|
264
|
-
this.code = code;
|
|
265
|
-
this.errors = errors;
|
|
266
|
-
this.method = options.method;
|
|
267
|
-
this.path = options.path;
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
644
|
var LIBRARY_VERSION = package_default.version;
|
|
271
645
|
var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
|
|
272
646
|
var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${package_default.version})`;
|
|
@@ -293,26 +667,6 @@ function isRequestData(value) {
|
|
|
293
667
|
return false;
|
|
294
668
|
return keys.every((key) => REQUEST_DATA_KEYS.has(key));
|
|
295
669
|
}
|
|
296
|
-
function serializeQuery(query) {
|
|
297
|
-
if (query === undefined)
|
|
298
|
-
return "";
|
|
299
|
-
if (typeof query === "string")
|
|
300
|
-
return query.length === 0 || query.startsWith("?") ? query : `?${query}`;
|
|
301
|
-
const params = query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => value === undefined || value === null ? [] : [[key, String(value)]]));
|
|
302
|
-
const serialized = params.toString();
|
|
303
|
-
return serialized.length === 0 ? "" : `?${serialized}`;
|
|
304
|
-
}
|
|
305
|
-
function majorParameter(path) {
|
|
306
|
-
const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/.exec(path);
|
|
307
|
-
if (!match)
|
|
308
|
-
return "@none";
|
|
309
|
-
if (match[1] === "webhooks" && match[3] !== undefined)
|
|
310
|
-
return `${match[2]}:${match[3]}`;
|
|
311
|
-
return match[2];
|
|
312
|
-
}
|
|
313
|
-
function scopeBucket(hashOrRoute, major) {
|
|
314
|
-
return `${hashOrRoute}|${major}`;
|
|
315
|
-
}
|
|
316
670
|
function createRetryPolicy(maxRetries = 2) {
|
|
317
671
|
if (!Number.isFinite(maxRetries) || maxRetries < 0)
|
|
318
672
|
throw new TypeError("maxRetries must be a non-negative finite number.");
|
|
@@ -320,7 +674,10 @@ function createRetryPolicy(maxRetries = 2) {
|
|
|
320
674
|
maxRetries: Math.floor(maxRetries),
|
|
321
675
|
shouldRetry(method, status) {
|
|
322
676
|
const normalized = method.toUpperCase();
|
|
323
|
-
|
|
677
|
+
const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(normalized);
|
|
678
|
+
if (status === 0)
|
|
679
|
+
return idempotent;
|
|
680
|
+
return status === 429 || status >= 500 && status <= 599 && idempotent;
|
|
324
681
|
},
|
|
325
682
|
getDelay(attempt, retryAfter) {
|
|
326
683
|
if (retryAfter !== undefined)
|
|
@@ -331,22 +688,32 @@ function createRetryPolicy(maxRetries = 2) {
|
|
|
331
688
|
}
|
|
332
689
|
|
|
333
690
|
class REST {
|
|
334
|
-
#baseURL;
|
|
335
|
-
#timeout;
|
|
336
691
|
#retryPolicy;
|
|
337
692
|
#token;
|
|
338
|
-
#
|
|
339
|
-
#
|
|
693
|
+
#limiter;
|
|
694
|
+
#scheduler = new RequestScheduler;
|
|
695
|
+
#transport;
|
|
696
|
+
#decoder = new ResponseDecoder;
|
|
340
697
|
#hooks = {};
|
|
698
|
+
#concurrentBuckets;
|
|
341
699
|
constructor(options = {}) {
|
|
342
700
|
this.#token = options.token;
|
|
343
|
-
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
344
701
|
this.#retryPolicy = options.retryPolicy ?? createRetryPolicy(options.retries ?? 2);
|
|
345
|
-
this.#
|
|
346
|
-
|
|
702
|
+
this.#transport = options.transport ?? new HttpTransport({
|
|
703
|
+
baseURL: options.baseURL,
|
|
704
|
+
timeout: options.timeout
|
|
705
|
+
});
|
|
706
|
+
const store = options.store ?? new MemoryRateLimitStore;
|
|
707
|
+
this.#concurrentBuckets = (options.concurrentBuckets ?? false) && typeof store.reserve === "function";
|
|
708
|
+
this.#limiter = new RateLimiter(store, {
|
|
709
|
+
concurrent: this.#concurrentBuckets
|
|
710
|
+
});
|
|
347
711
|
if (options.hooks)
|
|
348
712
|
this.#hooks = options.hooks;
|
|
349
713
|
}
|
|
714
|
+
get store() {
|
|
715
|
+
return this.#limiter.store;
|
|
716
|
+
}
|
|
350
717
|
setHooks(hooks) {
|
|
351
718
|
this.#hooks = hooks;
|
|
352
719
|
return this;
|
|
@@ -357,121 +724,110 @@ class REST {
|
|
|
357
724
|
this.#token = token;
|
|
358
725
|
}
|
|
359
726
|
async request(method, path, body, options = {}) {
|
|
360
|
-
|
|
361
|
-
if (!normalizedMethod)
|
|
727
|
+
if (!method.toUpperCase())
|
|
362
728
|
throw new TypeError("REST method is required.");
|
|
363
729
|
if (!path.startsWith("/"))
|
|
364
730
|
throw new TypeError("REST paths must start with '/'.");
|
|
731
|
+
const route = createRouteKey(method, path);
|
|
365
732
|
const files = options.files;
|
|
366
733
|
if (files && files.length > 0 && !(body instanceof FormData))
|
|
367
734
|
body = this.#fileForm(body, files);
|
|
368
735
|
const query = serializeQuery(options.query);
|
|
369
|
-
const
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
const
|
|
374
|
-
|
|
375
|
-
let
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
736
|
+
const requestPath = query ? `${path}${query}` : path;
|
|
737
|
+
return this.#scheduler.runResolved(() => this.#limiter.resolveBucketKey(route.route, route.major), options.signal, path, (bucketKey, release) => this.#attempts(route, requestPath, body, options, bucketKey, release));
|
|
738
|
+
}
|
|
739
|
+
async#attempts(route, requestPath, body, options, initialBucketKey, release) {
|
|
740
|
+
const { method, path } = route;
|
|
741
|
+
let bucketKey = initialBucketKey;
|
|
742
|
+
for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
|
|
743
|
+
await this.#limiter.acquire(bucketKey, options.signal, path);
|
|
744
|
+
if (this.#concurrentBuckets) {
|
|
745
|
+
const bucket = await this.#limiter.store.getBucket(bucketKey);
|
|
746
|
+
if (bucket && bucket.resetAt > Date.now())
|
|
747
|
+
release();
|
|
748
|
+
}
|
|
749
|
+
this.#emit(this.#hooks.onRequest, { method, path, attempt });
|
|
750
|
+
const requestStart = Date.now();
|
|
751
|
+
let response;
|
|
752
|
+
try {
|
|
753
|
+
response = await this.#transport.send({
|
|
754
|
+
method,
|
|
755
|
+
path: requestPath,
|
|
756
|
+
headers: this.#headers(body, options),
|
|
757
|
+
body: this.#encodeBody(body),
|
|
758
|
+
signal: options.signal
|
|
391
759
|
});
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
if (options.
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
headers["Content-Type"] = "application/json";
|
|
402
|
-
const fetchBody = isFormData ? body : body === undefined ? undefined : JSON.stringify(body);
|
|
403
|
-
try {
|
|
404
|
-
this.#emit(this.#hooks.onRequest, {
|
|
405
|
-
method: normalizedMethod,
|
|
406
|
-
path,
|
|
407
|
-
attempt
|
|
408
|
-
});
|
|
409
|
-
const requestStart = Date.now();
|
|
410
|
-
const response = await fetch(`${this.#baseURL}${finalPath}`, {
|
|
411
|
-
method: normalizedMethod,
|
|
412
|
-
headers,
|
|
413
|
-
body: fetchBody,
|
|
414
|
-
signal: controller.signal
|
|
415
|
-
});
|
|
416
|
-
this.#emit(this.#hooks.onResponse, {
|
|
417
|
-
method: normalizedMethod,
|
|
418
|
-
path,
|
|
419
|
-
status: response.status,
|
|
420
|
-
durationMs: Date.now() - requestStart
|
|
421
|
-
});
|
|
422
|
-
bucketKey = await this.#update(response, bucketKey, route, major);
|
|
423
|
-
const payload = await this.#readPayload(response);
|
|
424
|
-
if (response.ok)
|
|
425
|
-
return response.status === 204 ? undefined : payload;
|
|
426
|
-
const data = this.#errorData(payload);
|
|
427
|
-
const retryAfter = response.status === 429 ? this.#retryAfter(response, data) : undefined;
|
|
428
|
-
if (data.global && retryAfter !== undefined) {
|
|
429
|
-
const currentGlobal = await this.#store.getGlobalReset();
|
|
430
|
-
await this.#store.setGlobalReset(Math.max(currentGlobal, Date.now() + retryAfter * 1000));
|
|
431
|
-
}
|
|
432
|
-
if (response.status === 429)
|
|
433
|
-
this.#emit(this.#hooks.onRateLimit, {
|
|
434
|
-
method: normalizedMethod,
|
|
435
|
-
path,
|
|
436
|
-
retryAfterMs: (retryAfter ?? 1) * 1000,
|
|
437
|
-
global: data.global ?? false,
|
|
438
|
-
bucket: await this.#store.getBucketHash(route)
|
|
439
|
-
});
|
|
440
|
-
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, response.status)) {
|
|
441
|
-
const delay = this.#retryPolicy.getDelay(attempt, retryAfter);
|
|
442
|
-
this.#emit(this.#hooks.onRetry, {
|
|
443
|
-
method: normalizedMethod,
|
|
444
|
-
path,
|
|
445
|
-
attempt,
|
|
446
|
-
status: response.status,
|
|
447
|
-
delayMs: delay
|
|
448
|
-
});
|
|
449
|
-
await this.#sleep(delay, options.signal, path);
|
|
450
|
-
continue;
|
|
451
|
-
}
|
|
452
|
-
throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method: normalizedMethod, path });
|
|
453
|
-
} catch (error) {
|
|
454
|
-
if (error instanceof RESTError)
|
|
455
|
-
throw error;
|
|
456
|
-
if (options.signal?.aborted)
|
|
457
|
-
throw this.#abortError(path, options.signal.reason);
|
|
458
|
-
const isTimeout = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
459
|
-
if (!isTimeout && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, 0)) {
|
|
460
|
-
await this.#sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
|
|
461
|
-
continue;
|
|
462
|
-
}
|
|
463
|
-
throw new RESTError(isTimeout ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", 0, undefined, undefined, { method: normalizedMethod, path, cause: error });
|
|
464
|
-
} finally {
|
|
465
|
-
clearTimeout(timer);
|
|
466
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
760
|
+
} catch (error) {
|
|
761
|
+
if (error instanceof RESTError)
|
|
762
|
+
throw error;
|
|
763
|
+
if (options.signal?.aborted)
|
|
764
|
+
throw abortError(path, options.signal.reason);
|
|
765
|
+
const failure = error instanceof TransportError ? error : new TransportError("Discord REST request failed", false, error);
|
|
766
|
+
if (!failure.timedOut && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, 0)) {
|
|
767
|
+
await sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
|
|
768
|
+
continue;
|
|
467
769
|
}
|
|
770
|
+
throw new RESTError(failure.message, 0, undefined, undefined, {
|
|
771
|
+
method,
|
|
772
|
+
path,
|
|
773
|
+
cause: failure.cause ?? failure,
|
|
774
|
+
kind: failure.timedOut ? "timeout" : "network"
|
|
775
|
+
});
|
|
468
776
|
}
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
777
|
+
this.#emit(this.#hooks.onResponse, {
|
|
778
|
+
method,
|
|
779
|
+
path,
|
|
780
|
+
status: response.status,
|
|
781
|
+
durationMs: Date.now() - requestStart
|
|
782
|
+
});
|
|
783
|
+
bucketKey = await this.#limiter.applyResponse(response, bucketKey, route.route, route.major);
|
|
784
|
+
const payload = await this.#decoder.read(response);
|
|
785
|
+
if (response.ok)
|
|
786
|
+
return response.status === 204 ? undefined : payload;
|
|
787
|
+
const data = this.#decoder.errorData(payload);
|
|
788
|
+
const retryAfter = response.status === 429 ? this.#decoder.retryAfter(response, data) : undefined;
|
|
789
|
+
if (data.global && retryAfter !== undefined)
|
|
790
|
+
await this.#limiter.noteGlobalReset(Date.now() + retryAfter * 1000);
|
|
791
|
+
if (response.status === 429)
|
|
792
|
+
this.#emit(this.#hooks.onRateLimit, {
|
|
793
|
+
method,
|
|
794
|
+
path,
|
|
795
|
+
retryAfterMs: (retryAfter ?? 1) * 1000,
|
|
796
|
+
global: data.global ?? false,
|
|
797
|
+
bucket: await this.#limiter.store.getBucketHash(route.route)
|
|
798
|
+
});
|
|
799
|
+
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, response.status)) {
|
|
800
|
+
const delay = this.#retryPolicy.getDelay(attempt, retryAfter);
|
|
801
|
+
this.#emit(this.#hooks.onRetry, {
|
|
802
|
+
method,
|
|
803
|
+
path,
|
|
804
|
+
attempt,
|
|
805
|
+
status: response.status,
|
|
806
|
+
delayMs: delay
|
|
807
|
+
});
|
|
808
|
+
await sleep(delay, options.signal, path);
|
|
809
|
+
continue;
|
|
810
|
+
}
|
|
811
|
+
throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method, path });
|
|
474
812
|
}
|
|
813
|
+
throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method, path });
|
|
814
|
+
}
|
|
815
|
+
#headers(body, options) {
|
|
816
|
+
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
817
|
+
const headers = { ...options.headers };
|
|
818
|
+
if (options.auth !== false && this.#token)
|
|
819
|
+
headers["Authorization"] = `Bot ${this.#token}`;
|
|
820
|
+
headers["User-Agent"] = USER_AGENT;
|
|
821
|
+
if (options.reason !== undefined)
|
|
822
|
+
headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
|
|
823
|
+
if (!isFormData)
|
|
824
|
+
headers["Content-Type"] = "application/json";
|
|
825
|
+
return headers;
|
|
826
|
+
}
|
|
827
|
+
#encodeBody(body) {
|
|
828
|
+
if (typeof FormData !== "undefined" && body instanceof FormData)
|
|
829
|
+
return body;
|
|
830
|
+
return body === undefined ? undefined : JSON.stringify(body);
|
|
475
831
|
}
|
|
476
832
|
#dispatch(method, path, bodyOrOptions, options) {
|
|
477
833
|
if (options === undefined && isRequestData(bodyOrOptions))
|
|
@@ -514,127 +870,33 @@ class REST {
|
|
|
514
870
|
#emit(hook, ctx) {
|
|
515
871
|
if (!hook)
|
|
516
872
|
return;
|
|
873
|
+
const withPath = ctx;
|
|
874
|
+
if (typeof withPath.path === "string")
|
|
875
|
+
ctx = { ...ctx, path: redactPath(withPath.path) };
|
|
517
876
|
Promise.resolve().then(() => hook(ctx)).catch(() => {});
|
|
518
877
|
}
|
|
519
|
-
async#wait(bucketKey, signal, path) {
|
|
520
|
-
const bucket = await this.#store.getBucket(bucketKey);
|
|
521
|
-
if (!bucket)
|
|
522
|
-
return;
|
|
523
|
-
const delay = bucket.resetAt - Date.now();
|
|
524
|
-
if (bucket.remaining <= 0 && delay > 0)
|
|
525
|
-
await this.#sleep(delay, signal, path);
|
|
526
|
-
}
|
|
527
|
-
async#waitGlobal(signal, path) {
|
|
528
|
-
const globalResetAt = await this.#store.getGlobalReset();
|
|
529
|
-
const delay = globalResetAt - Date.now();
|
|
530
|
-
if (delay > 0)
|
|
531
|
-
await this.#sleep(delay, signal, path);
|
|
532
|
-
}
|
|
533
|
-
async#sleep(delay, signal, path) {
|
|
534
|
-
if (delay <= 0)
|
|
535
|
-
return;
|
|
536
|
-
await new Promise((resolve, reject) => {
|
|
537
|
-
const timer = setTimeout(resolve, delay);
|
|
538
|
-
const abort = () => {
|
|
539
|
-
clearTimeout(timer);
|
|
540
|
-
reject(this.#abortError(path, signal?.reason));
|
|
541
|
-
};
|
|
542
|
-
if (signal?.aborted)
|
|
543
|
-
return abort();
|
|
544
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
545
|
-
});
|
|
546
|
-
}
|
|
547
|
-
async#abortable(promise, signal, path) {
|
|
548
|
-
if (!signal)
|
|
549
|
-
return promise;
|
|
550
|
-
if (signal.aborted)
|
|
551
|
-
throw this.#abortError(path, signal.reason);
|
|
552
|
-
return Promise.race([
|
|
553
|
-
promise,
|
|
554
|
-
new Promise((_, reject) => signal.addEventListener("abort", () => reject(this.#abortError(path, signal.reason)), { once: true }))
|
|
555
|
-
]);
|
|
556
|
-
}
|
|
557
|
-
#abortError(path, reason) {
|
|
558
|
-
return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason });
|
|
559
|
-
}
|
|
560
|
-
async#readPayload(response) {
|
|
561
|
-
if (response.status === 204)
|
|
562
|
-
return;
|
|
563
|
-
const contentType = response.headers.get("content-type") ?? "";
|
|
564
|
-
if (contentType.includes("application/json"))
|
|
565
|
-
return response.json().catch(() => {
|
|
566
|
-
return;
|
|
567
|
-
});
|
|
568
|
-
return response.text().catch(() => {
|
|
569
|
-
return;
|
|
570
|
-
});
|
|
571
|
-
}
|
|
572
|
-
#errorData(payload) {
|
|
573
|
-
if (!payload || typeof payload !== "object")
|
|
574
|
-
return {};
|
|
575
|
-
const data = payload;
|
|
576
|
-
return {
|
|
577
|
-
message: typeof data.message === "string" ? data.message : undefined,
|
|
578
|
-
code: typeof data.code === "number" ? data.code : undefined,
|
|
579
|
-
retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
|
|
580
|
-
global: data.global === true
|
|
581
|
-
};
|
|
582
|
-
}
|
|
583
|
-
#retryAfter(response, data) {
|
|
584
|
-
const header = Number(response.headers.get("Retry-After"));
|
|
585
|
-
const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
|
|
586
|
-
return Math.max(0, retryAfter);
|
|
587
|
-
}
|
|
588
|
-
#normalizeRoute(path) {
|
|
589
|
-
const queryIndex = path.indexOf("?");
|
|
590
|
-
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
591
|
-
return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
592
|
-
}
|
|
593
|
-
async#update(response, bucketKey, route, major) {
|
|
594
|
-
let currentKey = bucketKey;
|
|
595
|
-
const serverBucket = response.headers.get("X-RateLimit-Bucket");
|
|
596
|
-
if (serverBucket) {
|
|
597
|
-
await this.#store.setBucketHash(route, serverBucket);
|
|
598
|
-
currentKey = scopeBucket(serverBucket, major);
|
|
599
|
-
}
|
|
600
|
-
let bucket = await this.#store.getBucket(currentKey) ?? {
|
|
601
|
-
remaining: 1,
|
|
602
|
-
resetAt: 0
|
|
603
|
-
};
|
|
604
|
-
const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
|
|
605
|
-
const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
|
|
606
|
-
const reset = Number(response.headers.get("X-RateLimit-Reset"));
|
|
607
|
-
if (Number.isFinite(remaining))
|
|
608
|
-
bucket.remaining = Math.max(0, remaining);
|
|
609
|
-
if (Number.isFinite(resetAfter))
|
|
610
|
-
bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
|
|
611
|
-
else if (Number.isFinite(reset))
|
|
612
|
-
bucket.resetAt = reset * 1000;
|
|
613
|
-
if (response.status === 429) {
|
|
614
|
-
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
615
|
-
if (Number.isFinite(retryAfter))
|
|
616
|
-
bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
|
|
617
|
-
}
|
|
618
|
-
await this.#store.updateBucket(currentKey, bucket);
|
|
619
|
-
if (response.headers.get("X-RateLimit-Global") === "true") {
|
|
620
|
-
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
621
|
-
if (Number.isFinite(retryAfter)) {
|
|
622
|
-
const currentGlobal = await this.#store.getGlobalReset();
|
|
623
|
-
await this.#store.setGlobalReset(Math.max(currentGlobal, Date.now() + retryAfter * 1000));
|
|
624
|
-
}
|
|
625
|
-
}
|
|
626
|
-
return currentKey;
|
|
627
|
-
}
|
|
628
878
|
}
|
|
629
879
|
export {
|
|
630
880
|
RESTError as DiscordAPIError,
|
|
881
|
+
HttpTransport,
|
|
631
882
|
MemoryRateLimitStore,
|
|
632
883
|
REST,
|
|
633
884
|
RESTError,
|
|
885
|
+
RateLimiter,
|
|
634
886
|
RedisRateLimitStore,
|
|
887
|
+
RequestScheduler,
|
|
888
|
+
ResponseDecoder,
|
|
635
889
|
Routes,
|
|
890
|
+
TransportError,
|
|
636
891
|
WebhookClient,
|
|
637
|
-
|
|
892
|
+
abortError,
|
|
893
|
+
createRetryPolicy,
|
|
894
|
+
createRouteKey,
|
|
895
|
+
majorParameter,
|
|
896
|
+
normalizeRoutePath,
|
|
897
|
+
redactPath,
|
|
898
|
+
scopeBucket,
|
|
899
|
+
serializeQuery
|
|
638
900
|
};
|
|
639
901
|
|
|
640
|
-
//# debugId=
|
|
902
|
+
//# debugId=3E6A1976705386FE64756E2164756E21
|