lunibee 0.1.5 → 0.1.7
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/README.md +13 -24
- package/dist/builders/commands.d.ts +37 -0
- package/dist/builders/components.d.ts +127 -12
- package/dist/builders/embed.d.ts +3 -1
- package/dist/builders/index.d.ts +2 -2
- package/dist/builders/index.js +319 -16
- package/dist/builders/index.js.map +8 -7
- package/dist/collection/index.d.ts +36 -0
- package/dist/collection/index.js +110 -6
- package/dist/collection/index.js.map +4 -4
- package/dist/core/events.d.ts +69 -3
- package/dist/core/index.d.ts +158 -90
- package/dist/core/index.js +1543 -1169
- package/dist/core/index.js.map +24 -23
- 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 +5 -4
- package/dist/index.js +3882 -1588
- package/dist/index.js.map +43 -35
- package/dist/managers/emoji.d.ts +38 -0
- package/dist/managers/guild.d.ts +141 -0
- package/dist/managers/index.d.ts +86 -75
- package/dist/managers/index.js +1084 -67
- package/dist/managers/index.js.map +21 -16
- package/dist/managers/message.d.ts +7 -12
- package/dist/managers/user.d.ts +20 -0
- package/dist/rest/index.d.ts +173 -1
- package/dist/rest/index.js +343 -47
- package/dist/rest/index.js.map +8 -6
- package/dist/rest/redis.d.ts +37 -0
- package/dist/rest/routes.d.ts +26 -1
- package/dist/rest/store.d.ts +38 -0
- package/dist/rest/webhook.d.ts +2 -2
- package/dist/sharding/cluster.d.ts +61 -0
- package/dist/sharding/index.d.ts +8 -0
- package/dist/sharding/index.js +289 -18
- package/dist/sharding/index.js.map +8 -7
- package/dist/structures/audit-log.d.ts +26 -0
- package/dist/structures/base.d.ts +131 -48
- package/dist/structures/embed.d.ts +65 -0
- package/dist/structures/index.d.ts +74 -26
- package/dist/structures/index.js +1528 -59
- package/dist/structures/index.js.map +14 -8
- package/dist/structures/interactions.d.ts +72 -17
- package/dist/structures/resources.d.ts +199 -7
- package/dist/types/index.d.ts +605 -84
- package/dist/types/index.js +71 -1
- package/dist/types/index.js.map +3 -3
- package/dist/utils/index.js.map +2 -2
- package/dist/voice/index.d.ts +88 -0
- package/dist/voice/index.js +204 -5
- package/dist/voice/index.js.map +3 -3
- package/dist/ws/index.d.ts +40 -1
- package/dist/ws/index.js +105 -18
- package/dist/ws/index.js.map +4 -4
- package/package.json +16 -16
- package/dist/structures/permissions.d.ts +0 -73
package/dist/rest/index.js
CHANGED
|
@@ -1,9 +1,64 @@
|
|
|
1
1
|
// @bun
|
|
2
|
+
// packages/rest/package.json
|
|
3
|
+
var package_default = {
|
|
4
|
+
name: "@lunibee/rest",
|
|
5
|
+
version: "0.1.7",
|
|
6
|
+
type: "module",
|
|
7
|
+
module: "./src/index.ts",
|
|
8
|
+
types: "./src/index.ts",
|
|
9
|
+
exports: {
|
|
10
|
+
".": "./src/index.ts"
|
|
11
|
+
}
|
|
12
|
+
};
|
|
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
|
+
updateBucket(key, state) {
|
|
31
|
+
this.#buckets.set(key, state);
|
|
32
|
+
const now = Date.now();
|
|
33
|
+
if (now - this.#lastPruneAt >= MemoryRateLimitStore.#PRUNE_INTERVAL) {
|
|
34
|
+
this.#lastPruneAt = now;
|
|
35
|
+
this.prune();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
prune(maxAge = 0) {
|
|
39
|
+
const cutoff = Date.now() - Math.max(0, maxAge);
|
|
40
|
+
for (const [key, state] of this.#buckets) {
|
|
41
|
+
if (state.resetAt <= cutoff)
|
|
42
|
+
this.#buckets.delete(key);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
getGlobalReset() {
|
|
46
|
+
return this.#globalResetAt;
|
|
47
|
+
}
|
|
48
|
+
setGlobalReset(resetAt) {
|
|
49
|
+
this.#globalResetAt = resetAt;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
2
53
|
// packages/rest/src/routes.ts
|
|
3
54
|
var Routes = {
|
|
4
55
|
user: () => "/users/@me",
|
|
5
56
|
userById: (userId) => `/users/${snowflake(userId, "User ID")}`,
|
|
6
57
|
guild: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}`,
|
|
58
|
+
guildPreview: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/preview`,
|
|
59
|
+
guildActiveThreads: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/threads/active`,
|
|
60
|
+
guildWebhooks: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/webhooks`,
|
|
61
|
+
guildInvites: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/invites`,
|
|
7
62
|
guildChannels: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/channels`,
|
|
8
63
|
guildMember: (guildId, userId) => `/guilds/${snowflake(guildId, "Guild ID")}/members/${snowflake(userId, "User ID")}`,
|
|
9
64
|
guildRoles: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/roles`,
|
|
@@ -31,7 +86,6 @@ var Routes = {
|
|
|
31
86
|
guildApplicationCommand: (applicationId, guildId, commandId) => `/applications/${snowflake(applicationId, "Application ID")}/guilds/${snowflake(guildId, "Guild ID")}/commands/${snowflake(commandId, "Command ID")}`,
|
|
32
87
|
interactionCallback: (interactionId, token) => `/interactions/${snowflake(interactionId, "Interaction ID")}/${encodeURIComponent(token)}/callback`,
|
|
33
88
|
interactionOriginalResponse: (applicationId, token) => `/webhooks/${snowflake(applicationId, "Application ID")}/${encodeURIComponent(token)}/messages/@original`,
|
|
34
|
-
guildActiveThreads: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/threads/active`,
|
|
35
89
|
channelPublicArchivedThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/threads/archived/public`,
|
|
36
90
|
channelPrivateArchivedThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/threads/archived/private`,
|
|
37
91
|
threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
|
|
@@ -41,13 +95,122 @@ var Routes = {
|
|
|
41
95
|
guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
|
|
42
96
|
voiceRegions: () => "/voice/regions",
|
|
43
97
|
gateway: () => "/gateway",
|
|
44
|
-
gatewayBot: () => "/gateway/bot"
|
|
98
|
+
gatewayBot: () => "/gateway/bot",
|
|
99
|
+
guildAuditLog: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/audit-logs`,
|
|
100
|
+
guildMembers: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/members`,
|
|
101
|
+
guildWelcomeScreen: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/welcome-screen`,
|
|
102
|
+
guildOnboarding: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/onboarding`,
|
|
103
|
+
guildPrune: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/prune`,
|
|
104
|
+
stageInstances: () => "/stage-instances",
|
|
105
|
+
stageInstanceByChannel: (channelId) => `/stage-instances/${snowflake(channelId, "Channel ID")}`,
|
|
106
|
+
invite: (code) => `/invites/${encodeURIComponent(code)}`,
|
|
107
|
+
guildVanity: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/vanity-url`,
|
|
108
|
+
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`
|
|
45
110
|
};
|
|
46
111
|
function snowflake(value, field) {
|
|
47
112
|
if (!/^\d{1,20}$/.test(value))
|
|
48
113
|
throw new TypeError(`${field} must be a valid Discord snowflake.`);
|
|
49
114
|
return value;
|
|
50
115
|
}
|
|
116
|
+
// packages/rest/src/redis.ts
|
|
117
|
+
class RedisRateLimitStore {
|
|
118
|
+
#client;
|
|
119
|
+
#prefix;
|
|
120
|
+
#onErrorCallback;
|
|
121
|
+
#lastError;
|
|
122
|
+
constructor(options) {
|
|
123
|
+
this.#client = options.client;
|
|
124
|
+
this.#prefix = options.prefix ?? "lunibee:rest:";
|
|
125
|
+
this.#onErrorCallback = options.onError;
|
|
126
|
+
}
|
|
127
|
+
get lastError() {
|
|
128
|
+
return this.#lastError;
|
|
129
|
+
}
|
|
130
|
+
isHealthy() {
|
|
131
|
+
return this.#lastError === undefined;
|
|
132
|
+
}
|
|
133
|
+
#onError(operation, error) {
|
|
134
|
+
this.#lastError = error;
|
|
135
|
+
console.warn(`[lunibee/rest] Redis rate-limit store ${operation} failed; ` + `falling back to local limiting for this call.`, error);
|
|
136
|
+
try {
|
|
137
|
+
this.#onErrorCallback?.(operation, error);
|
|
138
|
+
} catch {}
|
|
139
|
+
}
|
|
140
|
+
#markHealthy() {
|
|
141
|
+
this.#lastError = undefined;
|
|
142
|
+
}
|
|
143
|
+
async getBucketHash(route) {
|
|
144
|
+
try {
|
|
145
|
+
const hash = await this.#client.get(`${this.#prefix}route:${route}`);
|
|
146
|
+
this.#markHealthy();
|
|
147
|
+
return hash ?? undefined;
|
|
148
|
+
} catch (error) {
|
|
149
|
+
this.#onError("getBucketHash", error);
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
async setBucketHash(route, hash) {
|
|
154
|
+
try {
|
|
155
|
+
await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
|
|
156
|
+
this.#markHealthy();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
this.#onError("setBucketHash", error);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
async getBucket(key) {
|
|
162
|
+
try {
|
|
163
|
+
const data = await this.#client.get(`${this.#prefix}bucket:${key}`);
|
|
164
|
+
this.#markHealthy();
|
|
165
|
+
if (!data)
|
|
166
|
+
return;
|
|
167
|
+
return JSON.parse(data);
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.#onError("getBucket", error);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
async updateBucket(key, state) {
|
|
174
|
+
const bucketKey = `${this.#prefix}bucket:${key}`;
|
|
175
|
+
const ttl = Math.ceil((state.resetAt - Date.now()) / 1000);
|
|
176
|
+
try {
|
|
177
|
+
if (ttl <= 0) {
|
|
178
|
+
await this.#client.del(bucketKey);
|
|
179
|
+
this.#markHealthy();
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
await this.#client.set(bucketKey, JSON.stringify(state), "EX", ttl);
|
|
183
|
+
this.#markHealthy();
|
|
184
|
+
} catch (error) {
|
|
185
|
+
this.#onError("updateBucket", error);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
async getGlobalReset() {
|
|
189
|
+
try {
|
|
190
|
+
const resetAt = await this.#client.get(`${this.#prefix}global`);
|
|
191
|
+
this.#markHealthy();
|
|
192
|
+
return resetAt ? Number(resetAt) : 0;
|
|
193
|
+
} catch (error) {
|
|
194
|
+
this.#onError("getGlobalReset", error);
|
|
195
|
+
return 0;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
async setGlobalReset(resetAt) {
|
|
199
|
+
const globalKey = `${this.#prefix}global`;
|
|
200
|
+
const ttl = Math.ceil((resetAt - Date.now()) / 1000);
|
|
201
|
+
try {
|
|
202
|
+
if (ttl <= 0) {
|
|
203
|
+
await this.#client.del(globalKey);
|
|
204
|
+
this.#markHealthy();
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
await this.#client.set(globalKey, String(resetAt), "EX", ttl);
|
|
208
|
+
this.#markHealthy();
|
|
209
|
+
} catch (error) {
|
|
210
|
+
this.#onError("setGlobalReset", error);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
51
214
|
// packages/rest/src/webhook.ts
|
|
52
215
|
class WebhookClient {
|
|
53
216
|
id;
|
|
@@ -72,7 +235,7 @@ class WebhookClient {
|
|
|
72
235
|
const payload = typeof options === "string" ? { content: options } : options;
|
|
73
236
|
const { thread_id, ...body } = payload;
|
|
74
237
|
const query = thread_id ? `?thread_id=${thread_id}&wait=true` : `?wait=true`;
|
|
75
|
-
const formattedEmbeds = body.embeds?.map((e) =>
|
|
238
|
+
const formattedEmbeds = body.embeds?.map((e) => ("toJSON" in e) ? e.toJSON() : e);
|
|
76
239
|
return this.#rest.post(`${Routes.webhook(this.id, this.token)}${query}`, {
|
|
77
240
|
...body,
|
|
78
241
|
embeds: formattedEmbeds
|
|
@@ -104,6 +267,52 @@ class RESTError extends Error {
|
|
|
104
267
|
this.path = options.path;
|
|
105
268
|
}
|
|
106
269
|
}
|
|
270
|
+
var LIBRARY_VERSION = package_default.version;
|
|
271
|
+
var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
|
|
272
|
+
var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${package_default.version})`;
|
|
273
|
+
var REQUEST_DATA_KEYS = new Set([
|
|
274
|
+
"body",
|
|
275
|
+
"files",
|
|
276
|
+
"query",
|
|
277
|
+
"headers",
|
|
278
|
+
"reason",
|
|
279
|
+
"auth",
|
|
280
|
+
"signal"
|
|
281
|
+
]);
|
|
282
|
+
function isRequestData(value) {
|
|
283
|
+
if (value === null || typeof value !== "object")
|
|
284
|
+
return false;
|
|
285
|
+
if (Array.isArray(value))
|
|
286
|
+
return false;
|
|
287
|
+
if (typeof FormData !== "undefined" && value instanceof FormData)
|
|
288
|
+
return false;
|
|
289
|
+
if (value instanceof Blob || value instanceof Uint8Array || value instanceof ArrayBuffer)
|
|
290
|
+
return false;
|
|
291
|
+
const keys = Object.keys(value);
|
|
292
|
+
if (keys.length === 0)
|
|
293
|
+
return false;
|
|
294
|
+
return keys.every((key) => REQUEST_DATA_KEYS.has(key));
|
|
295
|
+
}
|
|
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
|
+
}
|
|
107
316
|
function createRetryPolicy(maxRetries = 2) {
|
|
108
317
|
if (!Number.isFinite(maxRetries) || maxRetries < 0)
|
|
109
318
|
throw new TypeError("maxRetries must be a non-negative finite number.");
|
|
@@ -126,14 +335,21 @@ class REST {
|
|
|
126
335
|
#timeout;
|
|
127
336
|
#retryPolicy;
|
|
128
337
|
#token;
|
|
129
|
-
#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
338
|
+
#store;
|
|
339
|
+
#localQueues = new Map;
|
|
340
|
+
#hooks = {};
|
|
132
341
|
constructor(options = {}) {
|
|
133
342
|
this.#token = options.token;
|
|
134
343
|
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
135
344
|
this.#retryPolicy = options.retryPolicy ?? createRetryPolicy(options.retries ?? 2);
|
|
136
345
|
this.#baseURL = (options.baseURL ?? "https://discord.com/api/v10").replace(/\/$/, "");
|
|
346
|
+
this.#store = options.store ?? new MemoryRateLimitStore;
|
|
347
|
+
if (options.hooks)
|
|
348
|
+
this.#hooks = options.hooks;
|
|
349
|
+
}
|
|
350
|
+
setHooks(hooks) {
|
|
351
|
+
this.#hooks = hooks;
|
|
352
|
+
return this;
|
|
137
353
|
}
|
|
138
354
|
setToken(token) {
|
|
139
355
|
if (!token.trim())
|
|
@@ -146,52 +362,91 @@ class REST {
|
|
|
146
362
|
throw new TypeError("REST method is required.");
|
|
147
363
|
if (!path.startsWith("/"))
|
|
148
364
|
throw new TypeError("REST paths must start with '/'.");
|
|
365
|
+
const files = options.files;
|
|
366
|
+
if (files && files.length > 0 && !(body instanceof FormData))
|
|
367
|
+
body = this.#fileForm(body, files);
|
|
368
|
+
const query = serializeQuery(options.query);
|
|
369
|
+
const finalPath = query ? `${path}${query}` : path;
|
|
149
370
|
const route = `${normalizedMethod}:${this.#normalizeRoute(path)}`;
|
|
150
|
-
|
|
151
|
-
let
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
queue: Promise.resolve()
|
|
155
|
-
};
|
|
156
|
-
this.#buckets.set(bucketKey, bucket);
|
|
157
|
-
const previous = bucket.queue;
|
|
371
|
+
const major = majorParameter(path);
|
|
372
|
+
let bucketKey = scopeBucket(await this.#store.getBucketHash(route) ?? route, major);
|
|
373
|
+
const queueKey = bucketKey;
|
|
374
|
+
const previous = this.#localQueues.get(queueKey) ?? Promise.resolve();
|
|
158
375
|
let release;
|
|
159
|
-
|
|
376
|
+
const gate = new Promise((resolve) => {
|
|
160
377
|
release = resolve;
|
|
161
378
|
});
|
|
379
|
+
this.#localQueues.set(queueKey, gate);
|
|
162
380
|
await this.#abortable(previous, options.signal, path);
|
|
163
381
|
try {
|
|
164
382
|
for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
|
|
165
|
-
await this.#wait(
|
|
383
|
+
await this.#wait(bucketKey, options.signal, path);
|
|
166
384
|
await this.#waitGlobal(options.signal, path);
|
|
167
385
|
const controller = new AbortController;
|
|
168
386
|
const onAbort = () => controller.abort(options.signal?.reason);
|
|
169
387
|
if (options.signal?.aborted)
|
|
170
388
|
throw this.#abortError(path, options.signal.reason);
|
|
171
|
-
options.signal?.addEventListener("abort", onAbort, {
|
|
389
|
+
options.signal?.addEventListener("abort", onAbort, {
|
|
390
|
+
once: true
|
|
391
|
+
});
|
|
172
392
|
const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
|
|
393
|
+
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
394
|
+
const headers = { ...options.headers };
|
|
395
|
+
if (options.auth !== false && this.#token)
|
|
396
|
+
headers["Authorization"] = `Bot ${this.#token}`;
|
|
397
|
+
headers["User-Agent"] = USER_AGENT;
|
|
398
|
+
if (options.reason !== undefined)
|
|
399
|
+
headers["X-Audit-Log-Reason"] = encodeURIComponent(options.reason);
|
|
400
|
+
if (!isFormData)
|
|
401
|
+
headers["Content-Type"] = "application/json";
|
|
402
|
+
const fetchBody = isFormData ? body : body === undefined ? undefined : JSON.stringify(body);
|
|
173
403
|
try {
|
|
174
|
-
|
|
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}`, {
|
|
175
411
|
method: normalizedMethod,
|
|
176
|
-
headers
|
|
177
|
-
|
|
178
|
-
"Content-Type": "application/json",
|
|
179
|
-
"User-Agent": "Lunibee/0.1.0"
|
|
180
|
-
},
|
|
181
|
-
body: body === undefined ? undefined : JSON.stringify(body),
|
|
412
|
+
headers,
|
|
413
|
+
body: fetchBody,
|
|
182
414
|
signal: controller.signal
|
|
183
415
|
});
|
|
184
|
-
|
|
185
|
-
|
|
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);
|
|
186
423
|
const payload = await this.#readPayload(response);
|
|
187
424
|
if (response.ok)
|
|
188
425
|
return response.status === 204 ? undefined : payload;
|
|
189
426
|
const data = this.#errorData(payload);
|
|
190
427
|
const retryAfter = response.status === 429 ? this.#retryAfter(response, data) : undefined;
|
|
191
|
-
if (data.global && retryAfter !== undefined)
|
|
192
|
-
|
|
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
|
+
});
|
|
193
440
|
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(normalizedMethod, response.status)) {
|
|
194
|
-
|
|
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);
|
|
195
450
|
continue;
|
|
196
451
|
}
|
|
197
452
|
throw new RESTError(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method: normalizedMethod, path });
|
|
@@ -214,30 +469,64 @@ class REST {
|
|
|
214
469
|
throw new RESTError("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method: normalizedMethod, path });
|
|
215
470
|
} finally {
|
|
216
471
|
release();
|
|
472
|
+
if (this.#localQueues.get(queueKey) === gate)
|
|
473
|
+
this.#localQueues.delete(queueKey);
|
|
217
474
|
}
|
|
218
475
|
}
|
|
476
|
+
#dispatch(method, path, bodyOrOptions, options) {
|
|
477
|
+
if (options === undefined && isRequestData(bodyOrOptions))
|
|
478
|
+
return this.request(method, path, bodyOrOptions.body, bodyOrOptions);
|
|
479
|
+
return this.request(method, path, bodyOrOptions, options);
|
|
480
|
+
}
|
|
219
481
|
get(path, options) {
|
|
220
|
-
return this.request("GET", path,
|
|
482
|
+
return this.request("GET", path, options?.body, options);
|
|
221
483
|
}
|
|
222
484
|
post(path, body, options) {
|
|
223
|
-
return this
|
|
485
|
+
return this.#dispatch("POST", path, body, options);
|
|
224
486
|
}
|
|
225
487
|
patch(path, body, options) {
|
|
226
|
-
return this
|
|
488
|
+
return this.#dispatch("PATCH", path, body, options);
|
|
227
489
|
}
|
|
228
490
|
put(path, body, options) {
|
|
229
|
-
return this
|
|
491
|
+
return this.#dispatch("PUT", path, body, options);
|
|
230
492
|
}
|
|
231
493
|
delete(path, options) {
|
|
232
|
-
return this.request("DELETE", path,
|
|
494
|
+
return this.request("DELETE", path, options?.body, options);
|
|
495
|
+
}
|
|
496
|
+
#fileForm(payload, files) {
|
|
497
|
+
const form = new FormData;
|
|
498
|
+
form.append("payload_json", JSON.stringify(payload));
|
|
499
|
+
for (let i = 0;i < files.length; i++) {
|
|
500
|
+
const file = files[i];
|
|
501
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data], {
|
|
502
|
+
type: file.contentType ?? "application/octet-stream"
|
|
503
|
+
});
|
|
504
|
+
form.append(`files[${i}]`, blob, file.name);
|
|
505
|
+
}
|
|
506
|
+
return form;
|
|
233
507
|
}
|
|
234
|
-
|
|
508
|
+
postWithFiles(path, payload, files, options) {
|
|
509
|
+
return this.request("POST", path, this.#fileForm(payload, files), options);
|
|
510
|
+
}
|
|
511
|
+
patchWithFiles(path, payload, files, options) {
|
|
512
|
+
return this.request("PATCH", path, this.#fileForm(payload, files), options);
|
|
513
|
+
}
|
|
514
|
+
#emit(hook, ctx) {
|
|
515
|
+
if (!hook)
|
|
516
|
+
return;
|
|
517
|
+
Promise.resolve().then(() => hook(ctx)).catch(() => {});
|
|
518
|
+
}
|
|
519
|
+
async#wait(bucketKey, signal, path) {
|
|
520
|
+
const bucket = await this.#store.getBucket(bucketKey);
|
|
521
|
+
if (!bucket)
|
|
522
|
+
return;
|
|
235
523
|
const delay = bucket.resetAt - Date.now();
|
|
236
524
|
if (bucket.remaining <= 0 && delay > 0)
|
|
237
525
|
await this.#sleep(delay, signal, path);
|
|
238
526
|
}
|
|
239
527
|
async#waitGlobal(signal, path) {
|
|
240
|
-
const
|
|
528
|
+
const globalResetAt = await this.#store.getGlobalReset();
|
|
529
|
+
const delay = globalResetAt - Date.now();
|
|
241
530
|
if (delay > 0)
|
|
242
531
|
await this.#sleep(delay, signal, path);
|
|
243
532
|
}
|
|
@@ -301,16 +590,17 @@ class REST {
|
|
|
301
590
|
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
302
591
|
return routePath.replace(/\/\d+(?=\/|$)/g, "/:id");
|
|
303
592
|
}
|
|
304
|
-
#update(response,
|
|
593
|
+
async#update(response, bucketKey, route, major) {
|
|
594
|
+
let currentKey = bucketKey;
|
|
305
595
|
const serverBucket = response.headers.get("X-RateLimit-Bucket");
|
|
306
596
|
if (serverBucket) {
|
|
307
|
-
this.#
|
|
308
|
-
|
|
309
|
-
const shared = this.#buckets.get(serverBucket) ?? bucket;
|
|
310
|
-
this.#buckets.set(serverBucket, shared);
|
|
311
|
-
bucket = shared;
|
|
312
|
-
}
|
|
597
|
+
await this.#store.setBucketHash(route, serverBucket);
|
|
598
|
+
currentKey = scopeBucket(serverBucket, major);
|
|
313
599
|
}
|
|
600
|
+
let bucket = await this.#store.getBucket(currentKey) ?? {
|
|
601
|
+
remaining: 1,
|
|
602
|
+
resetAt: 0
|
|
603
|
+
};
|
|
314
604
|
const remaining = Number(response.headers.get("X-RateLimit-Remaining"));
|
|
315
605
|
const resetAfter = Number(response.headers.get("X-RateLimit-Reset-After"));
|
|
316
606
|
const reset = Number(response.headers.get("X-RateLimit-Reset"));
|
|
@@ -325,20 +615,26 @@ class REST {
|
|
|
325
615
|
if (Number.isFinite(retryAfter))
|
|
326
616
|
bucket.resetAt = Math.max(bucket.resetAt, Date.now() + retryAfter * 1000);
|
|
327
617
|
}
|
|
618
|
+
await this.#store.updateBucket(currentKey, bucket);
|
|
328
619
|
if (response.headers.get("X-RateLimit-Global") === "true") {
|
|
329
620
|
const retryAfter = Number(response.headers.get("Retry-After"));
|
|
330
|
-
if (Number.isFinite(retryAfter))
|
|
331
|
-
|
|
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
|
+
}
|
|
332
625
|
}
|
|
333
|
-
return
|
|
626
|
+
return currentKey;
|
|
334
627
|
}
|
|
335
628
|
}
|
|
336
629
|
export {
|
|
630
|
+
RESTError as DiscordAPIError,
|
|
631
|
+
MemoryRateLimitStore,
|
|
337
632
|
REST,
|
|
338
633
|
RESTError,
|
|
634
|
+
RedisRateLimitStore,
|
|
339
635
|
Routes,
|
|
340
636
|
WebhookClient,
|
|
341
637
|
createRetryPolicy
|
|
342
638
|
};
|
|
343
639
|
|
|
344
|
-
//# debugId=
|
|
640
|
+
//# debugId=7273A79583E1900164756E2164756E21
|