lunibee 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/LICENSE +21 -21
- package/README.md +223 -223
- package/dist/builders/commands.d.ts +39 -0
- package/dist/builders/components.d.ts +37 -12
- package/dist/builders/embed.d.ts +3 -1
- package/dist/builders/index.d.ts +2 -2
- package/dist/builders/index.js +192 -29
- package/dist/builders/index.js.map +9 -7
- package/dist/collection/index.d.ts +30 -0
- package/dist/collection/index.js +79 -5
- package/dist/collection/index.js.map +4 -4
- package/dist/core/events.d.ts +88 -2
- package/dist/core/index.d.ts +46 -85
- package/dist/core/index.js +2395 -789
- package/dist/core/index.js.map +44 -22
- package/dist/core/permissions.d.ts +112 -53
- package/dist/formatters/index.d.ts +43 -3
- package/dist/formatters/index.js +64 -1
- package/dist/formatters/index.js.map +3 -3
- package/dist/handlers/index.js.map +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +3783 -1009
- package/dist/index.js.map +63 -38
- package/dist/managers/emoji.d.ts +38 -0
- package/dist/managers/guild-resources.d.ts +109 -0
- package/dist/managers/guild.d.ts +117 -1
- package/dist/managers/index.d.ts +37 -1
- package/dist/managers/index.js +1125 -81
- package/dist/managers/index.js.map +25 -20
- package/dist/rest/decoder.d.ts +23 -0
- package/dist/rest/errors.d.ts +20 -0
- package/dist/rest/index.d.ts +119 -25
- package/dist/rest/index.js +676 -204
- package/dist/rest/index.js.map +14 -6
- package/dist/rest/limiter.d.ts +42 -0
- package/dist/rest/redis.d.ts +54 -0
- package/dist/rest/route.d.ts +31 -0
- package/dist/rest/routes.d.ts +24 -0
- package/dist/rest/scheduler.d.ts +42 -0
- package/dist/rest/store.d.ts +67 -0
- package/dist/rest/transport.d.ts +40 -0
- package/dist/rest/webhook.d.ts +2 -2
- package/dist/sharding/bus.d.ts +31 -0
- package/dist/sharding/cluster.d.ts +71 -0
- package/dist/sharding/index.d.ts +18 -2
- package/dist/sharding/index.js +1202 -317
- package/dist/sharding/index.js.map +18 -7
- package/dist/structures/audit-log.d.ts +26 -0
- package/dist/structures/base.d.ts +40 -0
- package/dist/structures/channels.d.ts +56 -0
- package/dist/structures/index.d.ts +30 -1
- package/dist/structures/index.js +963 -87
- package/dist/structures/index.js.map +17 -10
- package/dist/structures/interactions.d.ts +12 -52
- package/dist/structures/options.d.ts +55 -0
- package/dist/structures/resources.d.ts +194 -6
- package/dist/types/gateway-events.d.ts +132 -0
- package/dist/types/gateway.d.ts +181 -0
- package/dist/types/index.d.ts +78 -263
- package/dist/types/index.js +44 -2
- package/dist/types/index.js.map +5 -4
- package/dist/utils/index.js.map +2 -2
- package/dist/voice/index.d.ts +116 -0
- package/dist/voice/index.js +261 -10
- 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 +24 -32
- package/dist/ws/index.js +920 -318
- 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 +23 -22
- package/dist/structures/permissions.d.ts +0 -73
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.1.
|
|
5
|
+
version: "0.1.8",
|
|
6
6
|
type: "module",
|
|
7
7
|
module: "./src/index.ts",
|
|
8
8
|
types: "./src/index.ts",
|
|
@@ -11,6 +11,351 @@ var package_default = {
|
|
|
11
11
|
}
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
+
// packages/rest/src/store.ts
|
|
15
|
+
class MemoryRateLimitStore {
|
|
16
|
+
#buckets = new Map;
|
|
17
|
+
#routeBuckets = new Map;
|
|
18
|
+
#globalResetAt = 0;
|
|
19
|
+
static #PRUNE_INTERVAL = 60000;
|
|
20
|
+
#lastPruneAt = 0;
|
|
21
|
+
getBucketHash(route) {
|
|
22
|
+
return this.#routeBuckets.get(route);
|
|
23
|
+
}
|
|
24
|
+
setBucketHash(route, hash) {
|
|
25
|
+
this.#routeBuckets.set(route, hash);
|
|
26
|
+
}
|
|
27
|
+
getBucket(key) {
|
|
28
|
+
return this.#buckets.get(key);
|
|
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
|
+
}
|
|
41
|
+
updateBucket(key, state) {
|
|
42
|
+
this.#buckets.set(key, state);
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
if (now - this.#lastPruneAt >= MemoryRateLimitStore.#PRUNE_INTERVAL) {
|
|
45
|
+
this.#lastPruneAt = now;
|
|
46
|
+
this.prune();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
prune(maxAge = 0) {
|
|
50
|
+
const cutoff = Date.now() - Math.max(0, maxAge);
|
|
51
|
+
for (const [key, state] of this.#buckets) {
|
|
52
|
+
if (state.resetAt <= cutoff)
|
|
53
|
+
this.#buckets.delete(key);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
getGlobalReset() {
|
|
57
|
+
return this.#globalResetAt;
|
|
58
|
+
}
|
|
59
|
+
setGlobalReset(resetAt) {
|
|
60
|
+
this.#globalResetAt = resetAt;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// packages/rest/src/errors.ts
|
|
65
|
+
class RESTError extends Error {
|
|
66
|
+
status;
|
|
67
|
+
code;
|
|
68
|
+
errors;
|
|
69
|
+
method;
|
|
70
|
+
path;
|
|
71
|
+
constructor(message, status, code, errors, options = {}) {
|
|
72
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
73
|
+
this.name = "RESTError";
|
|
74
|
+
this.status = status;
|
|
75
|
+
this.code = code;
|
|
76
|
+
this.errors = errors;
|
|
77
|
+
this.method = options.method;
|
|
78
|
+
this.path = options.path;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function abortError(path, reason) {
|
|
82
|
+
return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason });
|
|
83
|
+
}
|
|
84
|
+
async function sleep(delay, signal, path) {
|
|
85
|
+
if (delay <= 0)
|
|
86
|
+
return;
|
|
87
|
+
await new Promise((resolve, reject) => {
|
|
88
|
+
const timer = setTimeout(resolve, delay);
|
|
89
|
+
const abort = () => {
|
|
90
|
+
clearTimeout(timer);
|
|
91
|
+
reject(abortError(path, signal?.reason));
|
|
92
|
+
};
|
|
93
|
+
if (signal?.aborted)
|
|
94
|
+
return abort();
|
|
95
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
async function abortable(promise, signal, path) {
|
|
99
|
+
if (!signal)
|
|
100
|
+
return promise;
|
|
101
|
+
if (signal.aborted)
|
|
102
|
+
throw abortError(path, signal.reason);
|
|
103
|
+
return Promise.race([
|
|
104
|
+
promise,
|
|
105
|
+
new Promise((_, reject) => signal.addEventListener("abort", () => reject(abortError(path, signal.reason)), { once: true }))
|
|
106
|
+
]);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// packages/rest/src/route.ts
|
|
110
|
+
function normalizeRoutePath(path) {
|
|
111
|
+
const queryIndex = path.indexOf("?");
|
|
112
|
+
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
113
|
+
return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
114
|
+
}
|
|
115
|
+
function majorParameter(path) {
|
|
116
|
+
const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/.exec(path);
|
|
117
|
+
if (!match)
|
|
118
|
+
return "@none";
|
|
119
|
+
if (match[1] === "webhooks" && match[3] !== undefined)
|
|
120
|
+
return `${match[2]}:${match[3]}`;
|
|
121
|
+
return match[2];
|
|
122
|
+
}
|
|
123
|
+
function scopeBucket(hashOrRoute, major) {
|
|
124
|
+
return `${hashOrRoute}|${major}`;
|
|
125
|
+
}
|
|
126
|
+
function createRouteKey(method, path) {
|
|
127
|
+
const normalizedMethod = method.toUpperCase();
|
|
128
|
+
return {
|
|
129
|
+
method: normalizedMethod,
|
|
130
|
+
path,
|
|
131
|
+
route: `${normalizedMethod}:${normalizeRoutePath(path)}`,
|
|
132
|
+
major: majorParameter(path)
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
function serializeQuery(query) {
|
|
136
|
+
if (query === undefined)
|
|
137
|
+
return "";
|
|
138
|
+
if (typeof query === "string")
|
|
139
|
+
return query.length === 0 || query.startsWith("?") ? query : `?${query}`;
|
|
140
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => value === undefined || value === null ? [] : [[key, String(value)]]));
|
|
141
|
+
const serialized = params.toString();
|
|
142
|
+
return serialized.length === 0 ? "" : `?${serialized}`;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// packages/rest/src/scheduler.ts
|
|
146
|
+
class RequestScheduler {
|
|
147
|
+
static MAX_REMAPS = 2;
|
|
148
|
+
#queues = new Map;
|
|
149
|
+
get size() {
|
|
150
|
+
return this.#queues.size;
|
|
151
|
+
}
|
|
152
|
+
async runResolved(resolveKey, signal, path, task) {
|
|
153
|
+
let key = await resolveKey();
|
|
154
|
+
for (let hop = 0;hop < RequestScheduler.MAX_REMAPS; hop++) {
|
|
155
|
+
const outcome = await this.run(key, signal, path, async (release) => {
|
|
156
|
+
const current = await resolveKey();
|
|
157
|
+
if (current !== key)
|
|
158
|
+
return { done: false, key: current };
|
|
159
|
+
return { done: true, value: await task(key, release) };
|
|
160
|
+
});
|
|
161
|
+
if (outcome.done)
|
|
162
|
+
return outcome.value;
|
|
163
|
+
key = outcome.key;
|
|
164
|
+
}
|
|
165
|
+
return this.run(key, signal, path, (release) => task(key, release));
|
|
166
|
+
}
|
|
167
|
+
async run(key, signal, path, task) {
|
|
168
|
+
const previous = this.#queues.get(key) ?? Promise.resolve();
|
|
169
|
+
let open;
|
|
170
|
+
const gate = new Promise((resolve) => {
|
|
171
|
+
open = resolve;
|
|
172
|
+
});
|
|
173
|
+
this.#queues.set(key, gate);
|
|
174
|
+
let released = false;
|
|
175
|
+
const release = () => {
|
|
176
|
+
if (released)
|
|
177
|
+
return;
|
|
178
|
+
released = true;
|
|
179
|
+
open();
|
|
180
|
+
if (this.#queues.get(key) === gate)
|
|
181
|
+
this.#queues.delete(key);
|
|
182
|
+
};
|
|
183
|
+
try {
|
|
184
|
+
await abortable(previous, signal, path);
|
|
185
|
+
return await task(release);
|
|
186
|
+
} finally {
|
|
187
|
+
release();
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// packages/rest/src/limiter.ts
|
|
193
|
+
class RateLimiter {
|
|
194
|
+
#store;
|
|
195
|
+
#concurrent;
|
|
196
|
+
constructor(store, options = {}) {
|
|
197
|
+
this.#store = store;
|
|
198
|
+
this.#concurrent = options.concurrent ?? false;
|
|
199
|
+
}
|
|
200
|
+
get store() {
|
|
201
|
+
return this.#store;
|
|
202
|
+
}
|
|
203
|
+
async resolveBucketKey(route, major) {
|
|
204
|
+
return scopeBucket(await this.#store.getBucketHash(route) ?? route, major);
|
|
205
|
+
}
|
|
206
|
+
get reserves() {
|
|
207
|
+
return typeof this.#store.reserve === "function";
|
|
208
|
+
}
|
|
209
|
+
async acquire(bucketKey, signal, path) {
|
|
210
|
+
await this.#waitGlobal(signal, path);
|
|
211
|
+
if (!this.#store.reserve) {
|
|
212
|
+
const bucket = await this.#store.getBucket(bucketKey);
|
|
213
|
+
if (bucket) {
|
|
214
|
+
const delay = bucket.resetAt - Date.now();
|
|
215
|
+
if (bucket.remaining <= 0 && delay > 0)
|
|
216
|
+
await sleep(delay, signal, path);
|
|
217
|
+
}
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
for (let attempt = 0;attempt < 8; attempt++) {
|
|
221
|
+
const reservation = await this.#store.reserve(bucketKey);
|
|
222
|
+
if (reservation.granted)
|
|
223
|
+
return;
|
|
224
|
+
await sleep(Math.max(1, reservation.retryAfterMs), signal, path);
|
|
225
|
+
await this.#waitGlobal(signal, path);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
async#waitGlobal(signal, path) {
|
|
229
|
+
const globalDelay = await this.#store.getGlobalReset() - Date.now();
|
|
230
|
+
if (globalDelay > 0)
|
|
231
|
+
await sleep(globalDelay, signal, path);
|
|
232
|
+
}
|
|
233
|
+
async noteGlobalReset(resetAt) {
|
|
234
|
+
const current = await this.#store.getGlobalReset();
|
|
235
|
+
await this.#store.setGlobalReset(Math.max(current, resetAt));
|
|
236
|
+
}
|
|
237
|
+
async applyResponse(response, bucketKey, route, major) {
|
|
238
|
+
let currentKey = bucketKey;
|
|
239
|
+
const serverBucket = response.headers.get("X-RateLimit-Bucket");
|
|
240
|
+
if (serverBucket) {
|
|
241
|
+
await this.#store.setBucketHash(route, serverBucket);
|
|
242
|
+
currentKey = scopeBucket(serverBucket, major);
|
|
243
|
+
}
|
|
244
|
+
const stored = await this.#store.getBucket(currentKey);
|
|
245
|
+
const bucket = stored ? { ...stored } : { remaining: 1, resetAt: 0 };
|
|
246
|
+
const windowActive = this.#concurrent && !!stored && stored.resetAt > Date.now();
|
|
247
|
+
const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
|
|
248
|
+
const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
|
|
249
|
+
const reset = Number(response.headers.get("X-RateLimit-Reset"));
|
|
250
|
+
if (Number.isFinite(remaining))
|
|
251
|
+
bucket.remaining = Math.max(0, remaining);
|
|
252
|
+
if (Number.isFinite(resetAfter))
|
|
253
|
+
bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
|
|
254
|
+
else if (Number.isFinite(reset))
|
|
255
|
+
bucket.resetAt = reset * 1000;
|
|
256
|
+
if (windowActive && stored) {
|
|
257
|
+
bucket.remaining = Math.min(bucket.remaining, stored.remaining);
|
|
258
|
+
bucket.resetAt = Math.max(bucket.resetAt, stored.resetAt);
|
|
259
|
+
}
|
|
260
|
+
if (response.status === 429) {
|
|
261
|
+
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
262
|
+
if (Number.isFinite(retryAfter))
|
|
263
|
+
bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
|
|
264
|
+
}
|
|
265
|
+
await this.#store.updateBucket(currentKey, bucket);
|
|
266
|
+
if (response.headers.get("X-RateLimit-Global") === "true") {
|
|
267
|
+
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
268
|
+
if (Number.isFinite(retryAfter))
|
|
269
|
+
await this.noteGlobalReset(Date.now() + retryAfter * 1000);
|
|
270
|
+
}
|
|
271
|
+
return currentKey;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// packages/rest/src/transport.ts
|
|
276
|
+
class TransportError extends Error {
|
|
277
|
+
timedOut;
|
|
278
|
+
constructor(message, timedOut, cause) {
|
|
279
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
280
|
+
this.name = "TransportError";
|
|
281
|
+
this.timedOut = timedOut;
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
class HttpTransport {
|
|
286
|
+
#baseURL;
|
|
287
|
+
#timeout;
|
|
288
|
+
#fetch;
|
|
289
|
+
constructor(options = {}) {
|
|
290
|
+
this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
|
|
291
|
+
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
292
|
+
this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
293
|
+
}
|
|
294
|
+
get baseURL() {
|
|
295
|
+
return this.#baseURL;
|
|
296
|
+
}
|
|
297
|
+
get timeout() {
|
|
298
|
+
return this.#timeout;
|
|
299
|
+
}
|
|
300
|
+
async send(request) {
|
|
301
|
+
if (request.signal?.aborted)
|
|
302
|
+
throw abortError(request.path, request.signal.reason);
|
|
303
|
+
const controller = new AbortController;
|
|
304
|
+
const onAbort = () => controller.abort(request.signal?.reason);
|
|
305
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
306
|
+
const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
|
|
307
|
+
try {
|
|
308
|
+
return await this.#fetch(`${this.#baseURL}${request.path}`, {
|
|
309
|
+
method: request.method,
|
|
310
|
+
headers: request.headers,
|
|
311
|
+
body: request.body,
|
|
312
|
+
signal: controller.signal
|
|
313
|
+
});
|
|
314
|
+
} catch (error) {
|
|
315
|
+
if (error instanceof RESTError)
|
|
316
|
+
throw error;
|
|
317
|
+
const timedOut = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
318
|
+
throw new TransportError(timedOut ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", timedOut, error);
|
|
319
|
+
} finally {
|
|
320
|
+
clearTimeout(timer);
|
|
321
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// packages/rest/src/decoder.ts
|
|
327
|
+
class ResponseDecoder {
|
|
328
|
+
async read(response) {
|
|
329
|
+
if (response.status === 204)
|
|
330
|
+
return;
|
|
331
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
332
|
+
if (contentType.includes("application/json"))
|
|
333
|
+
return response.json().catch(() => {
|
|
334
|
+
return;
|
|
335
|
+
});
|
|
336
|
+
return response.text().catch(() => {
|
|
337
|
+
return;
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
errorData(payload) {
|
|
341
|
+
if (!payload || typeof payload !== "object")
|
|
342
|
+
return {};
|
|
343
|
+
const data = payload;
|
|
344
|
+
return {
|
|
345
|
+
message: typeof data.message === "string" ? data.message : undefined,
|
|
346
|
+
code: typeof data.code === "number" ? data.code : undefined,
|
|
347
|
+
retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
|
|
348
|
+
global: data.global === true
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
retryAfter(response, data) {
|
|
352
|
+
const raw = response.headers.get("Retry-After");
|
|
353
|
+
const header = raw === null ? Number.NaN : Number(raw);
|
|
354
|
+
const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
|
|
355
|
+
return Math.max(0, retryAfter);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
14
359
|
// packages/rest/src/routes.ts
|
|
15
360
|
var Routes = {
|
|
16
361
|
user: () => "/users/@me",
|
|
@@ -52,17 +397,169 @@ var Routes = {
|
|
|
52
397
|
threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
|
|
53
398
|
guildScheduledEvents: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events`,
|
|
54
399
|
guildScheduledEvent: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}`,
|
|
400
|
+
guildScheduledEventUsers: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}/users`,
|
|
401
|
+
channelPermission: (channelId, overwriteId) => `/channels/${snowflake(channelId, "Channel ID")}/permissions/${snowflake(overwriteId, "Overwrite ID")}`,
|
|
55
402
|
guildAutoModerationRules: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules`,
|
|
56
403
|
guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
|
|
57
404
|
voiceRegions: () => "/voice/regions",
|
|
58
405
|
gateway: () => "/gateway",
|
|
59
|
-
gatewayBot: () => "/gateway/bot"
|
|
406
|
+
gatewayBot: () => "/gateway/bot",
|
|
407
|
+
guildAuditLog: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/audit-logs`,
|
|
408
|
+
guildMembers: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/members`,
|
|
409
|
+
guildWelcomeScreen: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/welcome-screen`,
|
|
410
|
+
guildOnboarding: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/onboarding`,
|
|
411
|
+
guildPrune: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/prune`,
|
|
412
|
+
stageInstances: () => "/stage-instances",
|
|
413
|
+
stageInstanceByChannel: (channelId) => `/stage-instances/${snowflake(channelId, "Channel ID")}`,
|
|
414
|
+
invite: (code) => `/invites/${encodeURIComponent(code)}`,
|
|
415
|
+
guildVanity: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/vanity-url`,
|
|
416
|
+
guildEmoji: (guildId, emojiId) => emojiId ? `/guilds/${snowflake(guildId, "Guild ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/emojis`,
|
|
417
|
+
applicationEmoji: (applicationId, emojiId) => emojiId ? `/applications/${snowflake(applicationId, "Application ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/emojis`
|
|
60
418
|
};
|
|
61
419
|
function snowflake(value, field) {
|
|
62
420
|
if (!/^\d{1,20}$/.test(value))
|
|
63
421
|
throw new TypeError(`${field} must be a valid Discord snowflake.`);
|
|
64
422
|
return value;
|
|
65
423
|
}
|
|
424
|
+
// packages/rest/src/redis.ts
|
|
425
|
+
var RESERVE_SCRIPT = `
|
|
426
|
+
local raw = redis.call('GET', KEYS[1])
|
|
427
|
+
if not raw then return {1, 0} end
|
|
428
|
+
local ok, state = pcall(cjson.decode, raw)
|
|
429
|
+
if not ok or type(state) ~= 'table' or type(state.resetAt) ~= 'number' then
|
|
430
|
+
return {1, 0}
|
|
431
|
+
end
|
|
432
|
+
local now = tonumber(ARGV[1])
|
|
433
|
+
local wait = state.resetAt - now
|
|
434
|
+
if wait <= 0 then return {1, 0} end
|
|
435
|
+
if type(state.remaining) == 'number' and state.remaining > 0 then
|
|
436
|
+
state.remaining = state.remaining - 1
|
|
437
|
+
redis.call('SET', KEYS[1], cjson.encode(state), 'PX', math.ceil(wait))
|
|
438
|
+
return {1, 0}
|
|
439
|
+
end
|
|
440
|
+
return {0, math.ceil(wait)}
|
|
441
|
+
`;
|
|
442
|
+
|
|
443
|
+
class RedisRateLimitStore {
|
|
444
|
+
#client;
|
|
445
|
+
#prefix;
|
|
446
|
+
#onErrorCallback;
|
|
447
|
+
#local = new MemoryRateLimitStore;
|
|
448
|
+
#lastError;
|
|
449
|
+
constructor(options) {
|
|
450
|
+
this.#client = options.client;
|
|
451
|
+
this.#prefix = options.prefix ?? "lunibee:rest:";
|
|
452
|
+
this.#onErrorCallback = options.onError;
|
|
453
|
+
if (typeof options.client.eval === "function")
|
|
454
|
+
this.reserve = (key) => this.#reserve(key);
|
|
455
|
+
}
|
|
456
|
+
get lastError() {
|
|
457
|
+
return this.#lastError;
|
|
458
|
+
}
|
|
459
|
+
isHealthy() {
|
|
460
|
+
return this.#lastError === undefined;
|
|
461
|
+
}
|
|
462
|
+
#onError(operation, error) {
|
|
463
|
+
this.#lastError = error;
|
|
464
|
+
console.warn(`[lunibee/rest] Redis rate-limit store ${operation} failed; ` + `falling back to local limiting for this call.`, error);
|
|
465
|
+
try {
|
|
466
|
+
this.#onErrorCallback?.(operation, error);
|
|
467
|
+
} catch {}
|
|
468
|
+
}
|
|
469
|
+
#markHealthy() {
|
|
470
|
+
this.#lastError = undefined;
|
|
471
|
+
}
|
|
472
|
+
async getBucketHash(route) {
|
|
473
|
+
try {
|
|
474
|
+
const hash = await this.#client.get(`${this.#prefix}route:${route}`);
|
|
475
|
+
this.#markHealthy();
|
|
476
|
+
return hash ?? undefined;
|
|
477
|
+
} catch (error) {
|
|
478
|
+
this.#onError("getBucketHash", error);
|
|
479
|
+
return this.#local.getBucketHash(route);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
async setBucketHash(route, hash) {
|
|
483
|
+
this.#local.setBucketHash(route, hash);
|
|
484
|
+
try {
|
|
485
|
+
await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
|
|
486
|
+
this.#markHealthy();
|
|
487
|
+
} catch (error) {
|
|
488
|
+
this.#onError("setBucketHash", error);
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
async getBucket(key) {
|
|
492
|
+
try {
|
|
493
|
+
const data = await this.#client.get(`${this.#prefix}bucket:${key}`);
|
|
494
|
+
this.#markHealthy();
|
|
495
|
+
if (!data)
|
|
496
|
+
return;
|
|
497
|
+
return JSON.parse(data);
|
|
498
|
+
} catch (error) {
|
|
499
|
+
this.#onError("getBucket", error);
|
|
500
|
+
return this.#local.getBucket(key);
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
async updateBucket(key, state) {
|
|
504
|
+
this.#local.updateBucket(key, state);
|
|
505
|
+
const bucketKey = `${this.#prefix}bucket:${key}`;
|
|
506
|
+
const ttl = Math.ceil((state.resetAt - Date.now()) / 1000);
|
|
507
|
+
try {
|
|
508
|
+
if (ttl <= 0) {
|
|
509
|
+
await this.#client.del(bucketKey);
|
|
510
|
+
this.#markHealthy();
|
|
511
|
+
return;
|
|
512
|
+
}
|
|
513
|
+
await this.#client.set(bucketKey, JSON.stringify(state), "EX", ttl);
|
|
514
|
+
this.#markHealthy();
|
|
515
|
+
} catch (error) {
|
|
516
|
+
this.#onError("updateBucket", error);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
get supportsReservation() {
|
|
520
|
+
return typeof this.#client.eval === "function";
|
|
521
|
+
}
|
|
522
|
+
reserve;
|
|
523
|
+
async#reserve(key) {
|
|
524
|
+
try {
|
|
525
|
+
const result = await this.#client.eval(RESERVE_SCRIPT, 1, `${this.#prefix}bucket:${key}`, Date.now());
|
|
526
|
+
this.#markHealthy();
|
|
527
|
+
if (!Array.isArray(result))
|
|
528
|
+
return { granted: true };
|
|
529
|
+
const [granted, retryAfterMs] = result;
|
|
530
|
+
return Number(granted) === 1 ? { granted: true } : { granted: false, retryAfterMs: Number(retryAfterMs) || 0 };
|
|
531
|
+
} catch (error) {
|
|
532
|
+
this.#onError("reserve", error);
|
|
533
|
+
return this.#local.reserve(key);
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
async getGlobalReset() {
|
|
537
|
+
try {
|
|
538
|
+
const resetAt = await this.#client.get(`${this.#prefix}global`);
|
|
539
|
+
this.#markHealthy();
|
|
540
|
+
return resetAt ? Number(resetAt) : 0;
|
|
541
|
+
} catch (error) {
|
|
542
|
+
this.#onError("getGlobalReset", error);
|
|
543
|
+
return this.#local.getGlobalReset();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
async setGlobalReset(resetAt) {
|
|
547
|
+
this.#local.setGlobalReset(resetAt);
|
|
548
|
+
const globalKey = `${this.#prefix}global`;
|
|
549
|
+
const ttl = Math.ceil((resetAt - Date.now()) / 1000);
|
|
550
|
+
try {
|
|
551
|
+
if (ttl <= 0) {
|
|
552
|
+
await this.#client.del(globalKey);
|
|
553
|
+
this.#markHealthy();
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
await this.#client.set(globalKey, String(resetAt), "EX", ttl);
|
|
557
|
+
this.#markHealthy();
|
|
558
|
+
} catch (error) {
|
|
559
|
+
this.#onError("setGlobalReset", error);
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
}
|
|
66
563
|
// packages/rest/src/webhook.ts
|
|
67
564
|
class WebhookClient {
|
|
68
565
|
id;
|
|
@@ -87,7 +584,7 @@ class WebhookClient {
|
|
|
87
584
|
const payload = typeof options === "string" ? { content: options } : options;
|
|
88
585
|
const { thread_id, ...body } = payload;
|
|
89
586
|
const query = thread_id ? `?thread_id=${thread_id}&wait=true` : `?wait=true`;
|
|
90
|
-
const formattedEmbeds = body.embeds?.map((e) =>
|
|
587
|
+
const formattedEmbeds = body.embeds?.map((e) => ("toJSON" in e) ? e.toJSON() : e);
|
|
91
588
|
return this.#rest.post(`${Routes.webhook(this.id, this.token)}${query}`, {
|
|
92
589
|
...body,
|
|
93
590
|
embeds: formattedEmbeds
|
|
@@ -103,25 +600,32 @@ class WebhookClient {
|
|
|
103
600
|
}
|
|
104
601
|
|
|
105
602
|
// packages/rest/src/index.ts
|
|
106
|
-
class RESTError extends Error {
|
|
107
|
-
status;
|
|
108
|
-
code;
|
|
109
|
-
errors;
|
|
110
|
-
method;
|
|
111
|
-
path;
|
|
112
|
-
constructor(message, status, code, errors, options = {}) {
|
|
113
|
-
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
114
|
-
this.name = "RESTError";
|
|
115
|
-
this.status = status;
|
|
116
|
-
this.code = code;
|
|
117
|
-
this.errors = errors;
|
|
118
|
-
this.method = options.method;
|
|
119
|
-
this.path = options.path;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
603
|
var LIBRARY_VERSION = package_default.version;
|
|
123
604
|
var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
|
|
124
605
|
var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${package_default.version})`;
|
|
606
|
+
var REQUEST_DATA_KEYS = new Set([
|
|
607
|
+
"body",
|
|
608
|
+
"files",
|
|
609
|
+
"query",
|
|
610
|
+
"headers",
|
|
611
|
+
"reason",
|
|
612
|
+
"auth",
|
|
613
|
+
"signal"
|
|
614
|
+
]);
|
|
615
|
+
function isRequestData(value) {
|
|
616
|
+
if (value === null || typeof value !== "object")
|
|
617
|
+
return false;
|
|
618
|
+
if (Array.isArray(value))
|
|
619
|
+
return false;
|
|
620
|
+
if (typeof FormData !== "undefined" && value instanceof FormData)
|
|
621
|
+
return false;
|
|
622
|
+
if (value instanceof Blob || value instanceof Uint8Array || value instanceof ArrayBuffer)
|
|
623
|
+
return false;
|
|
624
|
+
const keys = Object.keys(value);
|
|
625
|
+
if (keys.length === 0)
|
|
626
|
+
return false;
|
|
627
|
+
return keys.every((key) => REQUEST_DATA_KEYS.has(key));
|
|
628
|
+
}
|
|
125
629
|
function createRetryPolicy(maxRetries = 2) {
|
|
126
630
|
if (!Number.isFinite(maxRetries) || maxRetries < 0)
|
|
127
631
|
throw new TypeError("maxRetries must be a non-negative finite number.");
|
|
@@ -129,7 +633,10 @@ function createRetryPolicy(maxRetries = 2) {
|
|
|
129
633
|
maxRetries: Math.floor(maxRetries),
|
|
130
634
|
shouldRetry(method, status) {
|
|
131
635
|
const normalized = method.toUpperCase();
|
|
132
|
-
|
|
636
|
+
const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(normalized);
|
|
637
|
+
if (status === 0)
|
|
638
|
+
return idempotent;
|
|
639
|
+
return status === 429 || status >= 500 && status <= 599 && idempotent;
|
|
133
640
|
},
|
|
134
641
|
getDelay(attempt, retryAfter) {
|
|
135
642
|
if (retryAfter !== undefined)
|
|
@@ -140,18 +647,35 @@ function createRetryPolicy(maxRetries = 2) {
|
|
|
140
647
|
}
|
|
141
648
|
|
|
142
649
|
class REST {
|
|
143
|
-
#baseURL;
|
|
144
|
-
#timeout;
|
|
145
650
|
#retryPolicy;
|
|
146
651
|
#token;
|
|
147
|
-
#
|
|
148
|
-
#
|
|
149
|
-
#
|
|
652
|
+
#limiter;
|
|
653
|
+
#scheduler = new RequestScheduler;
|
|
654
|
+
#transport;
|
|
655
|
+
#decoder = new ResponseDecoder;
|
|
656
|
+
#hooks = {};
|
|
657
|
+
#concurrentBuckets;
|
|
150
658
|
constructor(options = {}) {
|
|
151
659
|
this.#token = options.token;
|
|
152
|
-
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
153
660
|
this.#retryPolicy = options.retryPolicy ?? createRetryPolicy(options.retries ?? 2);
|
|
154
|
-
this.#
|
|
661
|
+
this.#transport = options.transport ?? new HttpTransport({
|
|
662
|
+
baseURL: options.baseURL,
|
|
663
|
+
timeout: options.timeout
|
|
664
|
+
});
|
|
665
|
+
const store = options.store ?? new MemoryRateLimitStore;
|
|
666
|
+
this.#concurrentBuckets = (options.concurrentBuckets ?? false) && typeof store.reserve === "function";
|
|
667
|
+
this.#limiter = new RateLimiter(store, {
|
|
668
|
+
concurrent: this.#concurrentBuckets
|
|
669
|
+
});
|
|
670
|
+
if (options.hooks)
|
|
671
|
+
this.#hooks = options.hooks;
|
|
672
|
+
}
|
|
673
|
+
get store() {
|
|
674
|
+
return this.#limiter.store;
|
|
675
|
+
}
|
|
676
|
+
setHooks(hooks) {
|
|
677
|
+
this.#hooks = hooks;
|
|
678
|
+
return this;
|
|
155
679
|
}
|
|
156
680
|
setToken(token) {
|
|
157
681
|
if (!token.trim())
|
|
@@ -159,99 +683,129 @@ class REST {
|
|
|
159
683
|
this.#token = token;
|
|
160
684
|
}
|
|
161
685
|
async request(method, path, body, options = {}) {
|
|
162
|
-
|
|
163
|
-
if (!normalizedMethod)
|
|
686
|
+
if (!method.toUpperCase())
|
|
164
687
|
throw new TypeError("REST method is required.");
|
|
165
688
|
if (!path.startsWith("/"))
|
|
166
689
|
throw new TypeError("REST paths must start with '/'.");
|
|
167
|
-
const route =
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
690
|
+
const route = createRouteKey(method, path);
|
|
691
|
+
const files = options.files;
|
|
692
|
+
if (files && files.length > 0 && !(body instanceof FormData))
|
|
693
|
+
body = this.#fileForm(body, files);
|
|
694
|
+
const query = serializeQuery(options.query);
|
|
695
|
+
const requestPath = query ? `${path}${query}` : path;
|
|
696
|
+
return this.#scheduler.runResolved(() => this.#limiter.resolveBucketKey(route.route, route.major), options.signal, path, (bucketKey, release) => this.#attempts(route, requestPath, body, options, bucketKey, release));
|
|
697
|
+
}
|
|
698
|
+
async#attempts(route, requestPath, body, options, initialBucketKey, release) {
|
|
699
|
+
const { method, path } = route;
|
|
700
|
+
let bucketKey = initialBucketKey;
|
|
701
|
+
for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
|
|
702
|
+
await this.#limiter.acquire(bucketKey, options.signal, path);
|
|
703
|
+
if (this.#concurrentBuckets) {
|
|
704
|
+
const bucket = await this.#limiter.store.getBucket(bucketKey);
|
|
705
|
+
if (bucket && bucket.resetAt > Date.now())
|
|
706
|
+
release();
|
|
707
|
+
}
|
|
708
|
+
this.#emit(this.#hooks.onRequest, { method, path, attempt });
|
|
709
|
+
const requestStart = Date.now();
|
|
710
|
+
let response;
|
|
711
|
+
try {
|
|
712
|
+
response = await this.#transport.send({
|
|
713
|
+
method,
|
|
714
|
+
path: requestPath,
|
|
715
|
+
headers: this.#headers(body, options),
|
|
716
|
+
body: this.#encodeBody(body),
|
|
717
|
+
signal: options.signal
|
|
718
|
+
});
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (error instanceof RESTError)
|
|
721
|
+
throw error;
|
|
187
722
|
if (options.signal?.aborted)
|
|
188
|
-
throw
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
...this.#token ? { Authorization: `Bot ${this.#token}` } : {},
|
|
194
|
-
"User-Agent": USER_AGENT
|
|
195
|
-
};
|
|
196
|
-
if (!isFormData)
|
|
197
|
-
headers["Content-Type"] = "application/json";
|
|
198
|
-
const fetchBody = isFormData ? body : body === undefined ? undefined : JSON.stringify(body);
|
|
199
|
-
try {
|
|
200
|
-
const response = await fetch(`${this.#baseURL}${path}`, {
|
|
201
|
-
method: normalizedMethod,
|
|
202
|
-
headers,
|
|
203
|
-
body: fetchBody,
|
|
204
|
-
signal: controller.signal
|
|
205
|
-
});
|
|
206
|
-
bucket = this.#update(response, bucket, route, bucketKey);
|
|
207
|
-
bucketKey = this.#routeBuckets.get(route) ?? bucketKey;
|
|
208
|
-
const payload = await this.#readPayload(response);
|
|
209
|
-
if (response.ok)
|
|
210
|
-
return response.status === 204 ? undefined : payload;
|
|
211
|
-
const data = this.#errorData(payload);
|
|
212
|
-
const retryAfter = response.status === 429 ? this.#retryAfter(response, data) : undefined;
|
|
213
|
-
if (data.global && retryAfter !== undefined)
|
|
214
|
-
this.#globalResetAt = Date.now() + retryAfter * 1000;
|
|
215
|
-
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, response.status)) {
|
|
216
|
-
await this.#sleep(this.#retryPolicy.getDelay(attempt, retryAfter), options.signal, path);
|
|
217
|
-
continue;
|
|
218
|
-
}
|
|
219
|
-
throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method: normalizedMethod, path });
|
|
220
|
-
} catch (error) {
|
|
221
|
-
if (error instanceof RESTError)
|
|
222
|
-
throw error;
|
|
223
|
-
if (options.signal?.aborted)
|
|
224
|
-
throw this.#abortError(path, options.signal.reason);
|
|
225
|
-
const isTimeout = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
226
|
-
if (!isTimeout && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, 0)) {
|
|
227
|
-
await this.#sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
|
|
228
|
-
continue;
|
|
229
|
-
}
|
|
230
|
-
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 });
|
|
231
|
-
} finally {
|
|
232
|
-
clearTimeout(timer);
|
|
233
|
-
options.signal?.removeEventListener("abort", onAbort);
|
|
723
|
+
throw abortError(path, options.signal.reason);
|
|
724
|
+
const failure = error instanceof TransportError ? error : new TransportError("Discord REST request failed", false, error);
|
|
725
|
+
if (!failure.timedOut && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, 0)) {
|
|
726
|
+
await sleep(this.#retryPolicy.getDelay(attempt), options.signal, path);
|
|
727
|
+
continue;
|
|
234
728
|
}
|
|
729
|
+
throw new RESTError(failure.message, 0, undefined, undefined, {
|
|
730
|
+
method,
|
|
731
|
+
path,
|
|
732
|
+
cause: failure.cause ?? failure
|
|
733
|
+
});
|
|
235
734
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
735
|
+
this.#emit(this.#hooks.onResponse, {
|
|
736
|
+
method,
|
|
737
|
+
path,
|
|
738
|
+
status: response.status,
|
|
739
|
+
durationMs: Date.now() - requestStart
|
|
740
|
+
});
|
|
741
|
+
bucketKey = await this.#limiter.applyResponse(response, bucketKey, route.route, route.major);
|
|
742
|
+
const payload = await this.#decoder.read(response);
|
|
743
|
+
if (response.ok)
|
|
744
|
+
return response.status === 204 ? undefined : payload;
|
|
745
|
+
const data = this.#decoder.errorData(payload);
|
|
746
|
+
const retryAfter = response.status === 429 ? this.#decoder.retryAfter(response, data) : undefined;
|
|
747
|
+
if (data.global && retryAfter !== undefined)
|
|
748
|
+
await this.#limiter.noteGlobalReset(Date.now() + retryAfter * 1000);
|
|
749
|
+
if (response.status === 429)
|
|
750
|
+
this.#emit(this.#hooks.onRateLimit, {
|
|
751
|
+
method,
|
|
752
|
+
path,
|
|
753
|
+
retryAfterMs: (retryAfter ?? 1) * 1000,
|
|
754
|
+
global: data.global ?? false,
|
|
755
|
+
bucket: await this.#limiter.store.getBucketHash(route.route)
|
|
756
|
+
});
|
|
757
|
+
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, response.status)) {
|
|
758
|
+
const delay = this.#retryPolicy.getDelay(attempt, retryAfter);
|
|
759
|
+
this.#emit(this.#hooks.onRetry, {
|
|
760
|
+
method,
|
|
761
|
+
path,
|
|
762
|
+
attempt,
|
|
763
|
+
status: response.status,
|
|
764
|
+
delayMs: delay
|
|
765
|
+
});
|
|
766
|
+
await sleep(delay, options.signal, path);
|
|
767
|
+
continue;
|
|
768
|
+
}
|
|
769
|
+
throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method, path });
|
|
239
770
|
}
|
|
771
|
+
throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method, path });
|
|
772
|
+
}
|
|
773
|
+
#headers(body, options) {
|
|
774
|
+
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
775
|
+
const headers = { ...options.headers };
|
|
776
|
+
if (options.auth !== false && this.#token)
|
|
777
|
+
headers["Authorization"] = `Bot ${this.#token}`;
|
|
778
|
+
headers["User-Agent"] = USER_AGENT;
|
|
779
|
+
if (options.reason !== undefined)
|
|
780
|
+
headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
|
|
781
|
+
if (!isFormData)
|
|
782
|
+
headers["Content-Type"] = "application/json";
|
|
783
|
+
return headers;
|
|
784
|
+
}
|
|
785
|
+
#encodeBody(body) {
|
|
786
|
+
if (typeof FormData !== "undefined" && body instanceof FormData)
|
|
787
|
+
return body;
|
|
788
|
+
return body === undefined ? undefined : JSON.stringify(body);
|
|
789
|
+
}
|
|
790
|
+
#dispatch(method, path, bodyOrOptions, options) {
|
|
791
|
+
if (options === undefined && isRequestData(bodyOrOptions))
|
|
792
|
+
return this.request(method, path, bodyOrOptions.body, bodyOrOptions);
|
|
793
|
+
return this.request(method, path, bodyOrOptions, options);
|
|
240
794
|
}
|
|
241
795
|
get(path, options) {
|
|
242
|
-
return this.request("GET", path,
|
|
796
|
+
return this.request("GET", path, options?.body, options);
|
|
243
797
|
}
|
|
244
798
|
post(path, body, options) {
|
|
245
|
-
return this
|
|
799
|
+
return this.#dispatch("POST", path, body, options);
|
|
246
800
|
}
|
|
247
801
|
patch(path, body, options) {
|
|
248
|
-
return this
|
|
802
|
+
return this.#dispatch("PATCH", path, body, options);
|
|
249
803
|
}
|
|
250
804
|
put(path, body, options) {
|
|
251
|
-
return this
|
|
805
|
+
return this.#dispatch("PUT", path, body, options);
|
|
252
806
|
}
|
|
253
807
|
delete(path, options) {
|
|
254
|
-
return this.request("DELETE", path,
|
|
808
|
+
return this.request("DELETE", path, options?.body, options);
|
|
255
809
|
}
|
|
256
810
|
#fileForm(payload, files) {
|
|
257
811
|
const form = new FormData;
|
|
@@ -271,114 +825,32 @@ class REST {
|
|
|
271
825
|
patchWithFiles(path, payload, files, options) {
|
|
272
826
|
return this.request("PATCH", path, this.#fileForm(payload, files), options);
|
|
273
827
|
}
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
if (bucket.remaining <= 0 && delay > 0)
|
|
277
|
-
await this.#sleep(delay, signal, path);
|
|
278
|
-
}
|
|
279
|
-
async#waitGlobal(signal, path) {
|
|
280
|
-
const delay = this.#globalResetAt - Date.now();
|
|
281
|
-
if (delay > 0)
|
|
282
|
-
await this.#sleep(delay, signal, path);
|
|
283
|
-
}
|
|
284
|
-
async#sleep(delay, signal, path) {
|
|
285
|
-
if (delay <= 0)
|
|
286
|
-
return;
|
|
287
|
-
await new Promise((resolve, reject) => {
|
|
288
|
-
const timer = setTimeout(resolve, delay);
|
|
289
|
-
const abort = () => {
|
|
290
|
-
clearTimeout(timer);
|
|
291
|
-
reject(this.#abortError(path, signal?.reason));
|
|
292
|
-
};
|
|
293
|
-
if (signal?.aborted)
|
|
294
|
-
return abort();
|
|
295
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
296
|
-
});
|
|
297
|
-
}
|
|
298
|
-
async#abortable(promise, signal, path) {
|
|
299
|
-
if (!signal)
|
|
300
|
-
return promise;
|
|
301
|
-
if (signal.aborted)
|
|
302
|
-
throw this.#abortError(path, signal.reason);
|
|
303
|
-
return Promise.race([
|
|
304
|
-
promise,
|
|
305
|
-
new Promise((_, reject) => signal.addEventListener("abort", () => reject(this.#abortError(path, signal.reason)), { once: true }))
|
|
306
|
-
]);
|
|
307
|
-
}
|
|
308
|
-
#abortError(path, reason) {
|
|
309
|
-
return new RESTError(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason });
|
|
310
|
-
}
|
|
311
|
-
async#readPayload(response) {
|
|
312
|
-
if (response.status === 204)
|
|
828
|
+
#emit(hook, ctx) {
|
|
829
|
+
if (!hook)
|
|
313
830
|
return;
|
|
314
|
-
|
|
315
|
-
if (contentType.includes("application/json"))
|
|
316
|
-
return response.json().catch(() => {
|
|
317
|
-
return;
|
|
318
|
-
});
|
|
319
|
-
return response.text().catch(() => {
|
|
320
|
-
return;
|
|
321
|
-
});
|
|
322
|
-
}
|
|
323
|
-
#errorData(payload) {
|
|
324
|
-
if (!payload || typeof payload !== "object")
|
|
325
|
-
return {};
|
|
326
|
-
const data = payload;
|
|
327
|
-
return {
|
|
328
|
-
message: typeof data.message === "string" ? data.message : undefined,
|
|
329
|
-
code: typeof data.code === "number" ? data.code : undefined,
|
|
330
|
-
retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
|
|
331
|
-
global: data.global === true
|
|
332
|
-
};
|
|
333
|
-
}
|
|
334
|
-
#retryAfter(response, data) {
|
|
335
|
-
const header = Number(response.headers.get("Retry-After"));
|
|
336
|
-
const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
|
|
337
|
-
return Math.max(0, retryAfter);
|
|
338
|
-
}
|
|
339
|
-
#normalizeRoute(path) {
|
|
340
|
-
const queryIndex = path.indexOf("?");
|
|
341
|
-
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
342
|
-
return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
343
|
-
}
|
|
344
|
-
#update(response, bucket, route, bucketKey) {
|
|
345
|
-
const serverBucket = response.headers.get("X-RateLimit-Bucket");
|
|
346
|
-
if (serverBucket) {
|
|
347
|
-
this.#routeBuckets.set(route, serverBucket);
|
|
348
|
-
if (serverBucket !== bucketKey) {
|
|
349
|
-
const shared = this.#buckets.get(serverBucket) ?? bucket;
|
|
350
|
-
this.#buckets.set(serverBucket, shared);
|
|
351
|
-
bucket = shared;
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
|
|
355
|
-
const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
|
|
356
|
-
const reset = Number(response.headers.get("X-RateLimit-Reset"));
|
|
357
|
-
if (Number.isFinite(remaining))
|
|
358
|
-
bucket.remaining = Math.max(0, remaining);
|
|
359
|
-
if (Number.isFinite(resetAfter))
|
|
360
|
-
bucket.resetAt = Date.now() + Math.max(0, resetAfter) * 1000;
|
|
361
|
-
else if (Number.isFinite(reset))
|
|
362
|
-
bucket.resetAt = reset * 1000;
|
|
363
|
-
if (response.status === 429) {
|
|
364
|
-
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
365
|
-
if (Number.isFinite(retryAfter))
|
|
366
|
-
bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
|
|
367
|
-
}
|
|
368
|
-
if (response.headers.get("X-RateLimit-Global") === "true") {
|
|
369
|
-
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
370
|
-
if (Number.isFinite(retryAfter))
|
|
371
|
-
this.#globalResetAt = Math.max(this.#globalResetAt, Date.now() + retryAfter * 1000);
|
|
372
|
-
}
|
|
373
|
-
return bucket;
|
|
831
|
+
Promise.resolve().then(() => hook(ctx)).catch(() => {});
|
|
374
832
|
}
|
|
375
833
|
}
|
|
376
834
|
export {
|
|
835
|
+
RESTError as DiscordAPIError,
|
|
836
|
+
HttpTransport,
|
|
837
|
+
MemoryRateLimitStore,
|
|
377
838
|
REST,
|
|
378
839
|
RESTError,
|
|
840
|
+
RateLimiter,
|
|
841
|
+
RedisRateLimitStore,
|
|
842
|
+
RequestScheduler,
|
|
843
|
+
ResponseDecoder,
|
|
379
844
|
Routes,
|
|
845
|
+
TransportError,
|
|
380
846
|
WebhookClient,
|
|
381
|
-
|
|
847
|
+
abortError,
|
|
848
|
+
createRetryPolicy,
|
|
849
|
+
createRouteKey,
|
|
850
|
+
majorParameter,
|
|
851
|
+
normalizeRoutePath,
|
|
852
|
+
scopeBucket,
|
|
853
|
+
serializeQuery
|
|
382
854
|
};
|
|
383
855
|
|
|
384
|
-
//# debugId=
|
|
856
|
+
//# debugId=CCF20CDF4862826C64756E2164756E21
|