lunibee 0.3.3 → 0.4.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/README.md +1 -1
- package/dist/builders/commands.d.ts +1 -1
- package/dist/builders/components.d.ts +9 -11
- package/dist/builders/embed.d.ts +1 -1
- package/dist/builders/index.d.ts +1 -1
- package/dist/builders/index.js +87 -1298
- package/dist/builders/index.js.map +3 -11
- package/dist/chunks/index-0wn81eng.js +2492 -0
- package/dist/chunks/index-0wn81eng.js.map +17 -0
- package/dist/chunks/index-2en489j6.js +40 -0
- package/dist/chunks/index-2en489j6.js.map +10 -0
- package/dist/chunks/index-3hyps48a.js +203 -0
- package/dist/chunks/index-3hyps48a.js.map +11 -0
- package/dist/chunks/index-57c5n3ms.js +2274 -0
- package/dist/chunks/index-57c5n3ms.js.map +19 -0
- package/dist/chunks/index-5yx92e36.js +1170 -0
- package/dist/chunks/index-5yx92e36.js.map +14 -0
- package/dist/chunks/index-690bp093.js +95 -0
- package/dist/chunks/index-690bp093.js.map +10 -0
- package/dist/chunks/index-7fp2hvmg.js +2155 -0
- package/dist/chunks/index-7fp2hvmg.js.map +24 -0
- package/dist/chunks/index-cnmsajvk.js +1119 -0
- package/dist/chunks/index-cnmsajvk.js.map +12 -0
- package/dist/chunks/index-fdvgk2sw.js +1310 -0
- package/dist/chunks/index-fdvgk2sw.js.map +21 -0
- package/dist/chunks/index-gyvps8b9.js +214 -0
- package/dist/chunks/index-gyvps8b9.js.map +12 -0
- package/dist/chunks/index-jzg4bh99.js +1533 -0
- package/dist/chunks/index-jzg4bh99.js.map +21 -0
- package/dist/chunks/index-mgw2yvrm.js +3547 -0
- package/dist/chunks/index-mgw2yvrm.js.map +25 -0
- package/dist/chunks/index-yh76b55h.js +3449 -0
- package/dist/chunks/index-yh76b55h.js.map +29 -0
- package/dist/collection/index.js +9 -1118
- package/dist/collection/index.js.map +3 -6
- package/dist/core/api.d.ts +3 -3
- package/dist/core/channel-create.d.ts +2 -2
- package/dist/core/commands.d.ts +3 -3
- package/dist/core/events.d.ts +4 -4
- package/dist/core/handles.d.ts +4 -4
- package/dist/core/index.d.ts +25 -20
- package/dist/core/index.js +68 -10130
- package/dist/core/index.js.map +3 -65
- package/dist/core/permissions.d.ts +1 -1
- package/dist/core/redis-cache.d.ts +1 -1
- package/dist/formatters/index.js +41 -110
- package/dist/formatters/index.js.map +3 -4
- package/dist/handlers/index.js +5 -37
- package/dist/handlers/index.js.map +3 -4
- package/dist/index.d.ts +21 -21
- package/dist/index.js +641 -16325
- package/dist/index.js.map +3 -92
- package/dist/managers/advanced.d.ts +3 -3
- package/dist/managers/application.d.ts +2 -2
- package/dist/managers/base.d.ts +1 -1
- package/dist/managers/emoji.d.ts +2 -2
- package/dist/managers/guild-resources.d.ts +6 -6
- package/dist/managers/guild.d.ts +3 -3
- package/dist/managers/index.d.ts +4 -8
- package/dist/managers/index.js +51 -4060
- package/dist/managers/index.js.map +3 -29
- package/dist/managers/member.d.ts +2 -2
- package/dist/managers/message.d.ts +14 -6
- package/dist/managers/role.d.ts +2 -2
- package/dist/managers/thread.d.ts +3 -3
- package/dist/managers/user.d.ts +7 -4
- package/dist/rest/index.d.ts +10 -35
- package/dist/rest/index.js +57 -2184
- package/dist/rest/index.js.map +3 -18
- package/dist/rest/limiter.d.ts +9 -1
- package/dist/rest/webhook.d.ts +2 -8
- package/dist/sharding/bus.d.ts +2 -2
- package/dist/sharding/cluster.d.ts +45 -5
- package/dist/sharding/context.d.ts +45 -3
- package/dist/sharding/gateway-bot.d.ts +2 -1
- package/dist/sharding/health.d.ts +1 -1
- package/dist/sharding/identify.d.ts +12 -0
- package/dist/sharding/index.d.ts +44 -11
- package/dist/sharding/index.js +60 -4620
- package/dist/sharding/index.js.map +3 -33
- package/dist/sharding/supervisor.d.ts +4 -2
- package/dist/structures/audit-log.d.ts +1 -1
- package/dist/structures/base.d.ts +6 -6
- package/dist/structures/channels.d.ts +1 -1
- package/dist/structures/embed.d.ts +1 -1
- package/dist/structures/index.d.ts +14 -14
- package/dist/structures/index.js +109 -2382
- package/dist/structures/index.js.map +3 -16
- package/dist/structures/interactions.d.ts +13 -9
- package/dist/structures/options.d.ts +1 -1
- package/dist/structures/permissions.d.ts +2 -2
- package/dist/structures/resources.d.ts +10 -10
- package/dist/structures/sticker.d.ts +1 -1
- package/dist/types/gateway.d.ts +2 -79
- package/dist/types/index.d.ts +0 -6
- package/dist/types/index.js +41 -272
- package/dist/types/index.js.map +3 -6
- package/dist/utils/index.js +23 -209
- package/dist/utils/index.js.map +3 -5
- package/dist/voice/connection.d.ts +200 -0
- package/dist/voice/crypto.d.ts +50 -0
- package/dist/voice/dave.d.ts +85 -0
- package/dist/voice/effects.d.ts +31 -0
- package/dist/voice/emitter.d.ts +14 -0
- package/dist/voice/gateway.d.ts +59 -0
- package/dist/voice/index.d.ts +25 -228
- package/dist/voice/index.js +111 -442
- package/dist/voice/index.js.map +3 -4
- package/dist/voice/libopus.d.ts +88 -0
- package/dist/voice/networking.d.ts +80 -0
- package/dist/voice/ogg.d.ts +24 -0
- package/dist/voice/opus.d.ts +18 -0
- package/dist/voice/player.d.ts +79 -0
- package/dist/voice/protocol.d.ts +76 -0
- package/dist/voice/receiver.d.ts +59 -0
- package/dist/voice/resource.d.ts +102 -0
- package/dist/voice/udp.d.ts +40 -0
- package/dist/ws/close-codes.d.ts +1 -1
- package/dist/ws/index.d.ts +25 -4
- package/dist/ws/index.js +47 -1336
- package/dist/ws/index.js.map +3 -17
- package/dist/ws/protocol.d.ts +1 -1
- package/dist/ws/send-budget.d.ts +0 -2
- package/dist/ws/state.d.ts +0 -10
- package/package.json +19 -16
|
@@ -0,0 +1,2155 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// packages/rest/package.json
|
|
3
|
+
var package_default = {
|
|
4
|
+
name: "@lunibee/rest",
|
|
5
|
+
version: "0.4.0",
|
|
6
|
+
type: "module",
|
|
7
|
+
module: "./src/index.ts",
|
|
8
|
+
types: "./src/index.ts",
|
|
9
|
+
exports: {
|
|
10
|
+
".": "./src/index.ts"
|
|
11
|
+
},
|
|
12
|
+
dependencies: {
|
|
13
|
+
"@lunibee/types": "workspace:*"
|
|
14
|
+
}
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// packages/rest/src/store.ts
|
|
18
|
+
var GLOBAL_WINDOW_MS = 1050;
|
|
19
|
+
function takeGlobalSlot(log, now, limit, window = GLOBAL_WINDOW_MS) {
|
|
20
|
+
while (log.length > 0 && log[0] <= now - window)
|
|
21
|
+
log.shift();
|
|
22
|
+
if (log.length >= limit)
|
|
23
|
+
return Math.max(1, log[0] + window - now + 1);
|
|
24
|
+
log.push(now);
|
|
25
|
+
return 0;
|
|
26
|
+
}
|
|
27
|
+
var PROBE_POLL_MS = 50;
|
|
28
|
+
var DEFAULT_PROBE_MS = 16000;
|
|
29
|
+
var BUCKET_LINGER_MS = 60000;
|
|
30
|
+
var WINDOW_TOLERANCE_MS = 50;
|
|
31
|
+
function reserveState(state, now, probeMs) {
|
|
32
|
+
if (state && state.resetAt > now) {
|
|
33
|
+
if (state.remaining > 0)
|
|
34
|
+
return {
|
|
35
|
+
reservation: {
|
|
36
|
+
granted: true,
|
|
37
|
+
known: true,
|
|
38
|
+
resetAt: state.resetAt
|
|
39
|
+
},
|
|
40
|
+
next: { ...state, remaining: state.remaining - 1 }
|
|
41
|
+
};
|
|
42
|
+
const wait = state.resetAt - now;
|
|
43
|
+
return {
|
|
44
|
+
reservation: {
|
|
45
|
+
granted: false,
|
|
46
|
+
retryAfterMs: state.provisional ? Math.min(wait, PROBE_POLL_MS) : wait,
|
|
47
|
+
reason: state.probe ? "probe" : "bucket"
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
const resetAt = now + probeMs;
|
|
52
|
+
if (state?.limit !== undefined && state.limit > 0)
|
|
53
|
+
return {
|
|
54
|
+
reservation: { granted: true, known: true, resetAt },
|
|
55
|
+
next: {
|
|
56
|
+
remaining: state.limit - 1,
|
|
57
|
+
resetAt,
|
|
58
|
+
limit: state.limit,
|
|
59
|
+
provisional: true
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
return {
|
|
63
|
+
reservation: { granted: true, known: false, resetAt },
|
|
64
|
+
next: { remaining: 0, resetAt, provisional: true, probe: true }
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
function reconcileState(state, update, now) {
|
|
68
|
+
const incoming = update.resetAfterMs === undefined ? undefined : Math.ceil(now + update.resetAfterMs);
|
|
69
|
+
const active = !!state && state.resetAt > now && !state.probe;
|
|
70
|
+
let next;
|
|
71
|
+
if (active) {
|
|
72
|
+
next = { ...state };
|
|
73
|
+
const stale = update.window !== undefined && next.window !== undefined && update.window < next.window - WINDOW_TOLERANCE_MS;
|
|
74
|
+
if (!stale) {
|
|
75
|
+
if (update.remaining !== undefined)
|
|
76
|
+
next.remaining = Math.min(next.remaining, update.remaining);
|
|
77
|
+
if (incoming !== undefined) {
|
|
78
|
+
const newer = update.window !== undefined && next.window !== undefined && update.window > next.window + WINDOW_TOLERANCE_MS;
|
|
79
|
+
next.resetAt = next.provisional || newer ? incoming : Math.max(next.resetAt, incoming);
|
|
80
|
+
delete next.provisional;
|
|
81
|
+
}
|
|
82
|
+
if (update.window !== undefined)
|
|
83
|
+
next.window = Math.max(next.window ?? 0, update.window);
|
|
84
|
+
}
|
|
85
|
+
} else {
|
|
86
|
+
if (update.remaining === undefined && update.retryAfterMs === undefined) {
|
|
87
|
+
return state?.probe ? null : undefined;
|
|
88
|
+
}
|
|
89
|
+
next = {
|
|
90
|
+
remaining: update.remaining ?? 0,
|
|
91
|
+
resetAt: incoming ?? now
|
|
92
|
+
};
|
|
93
|
+
if (state?.limit !== undefined)
|
|
94
|
+
next.limit = state.limit;
|
|
95
|
+
if (update.window !== undefined)
|
|
96
|
+
next.window = update.window;
|
|
97
|
+
}
|
|
98
|
+
if (update.limit !== undefined && update.limit > 0)
|
|
99
|
+
next.limit = update.limit;
|
|
100
|
+
if (update.retryAfterMs !== undefined) {
|
|
101
|
+
next.remaining = 0;
|
|
102
|
+
next.resetAt = Math.max(next.resetAt, Math.ceil(now + update.retryAfterMs));
|
|
103
|
+
delete next.provisional;
|
|
104
|
+
}
|
|
105
|
+
return next;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
class MemoryRateLimitStore2 {
|
|
109
|
+
#buckets = new Map;
|
|
110
|
+
#routeBuckets = new Map;
|
|
111
|
+
#globals = new Map;
|
|
112
|
+
#globalLogs = new Map;
|
|
113
|
+
static #PRUNE_INTERVAL = 60000;
|
|
114
|
+
#lastPruneAt = 0;
|
|
115
|
+
static #MAX_ENTRIES = 1e4;
|
|
116
|
+
reservesGlobal = true;
|
|
117
|
+
constructor() {}
|
|
118
|
+
static #evict(map) {
|
|
119
|
+
while (map.size > MemoryRateLimitStore2.#MAX_ENTRIES)
|
|
120
|
+
map.delete(map.keys().next().value);
|
|
121
|
+
}
|
|
122
|
+
getBucketHash(route) {
|
|
123
|
+
return this.#routeBuckets.get(route);
|
|
124
|
+
}
|
|
125
|
+
setBucketHash(route, hash) {
|
|
126
|
+
this.#routeBuckets.delete(route);
|
|
127
|
+
this.#routeBuckets.set(route, hash);
|
|
128
|
+
MemoryRateLimitStore2.#evict(this.#routeBuckets);
|
|
129
|
+
}
|
|
130
|
+
getBucket(key) {
|
|
131
|
+
return this.#buckets.get(key);
|
|
132
|
+
}
|
|
133
|
+
reserve(key, options = {}) {
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
if (options.global !== undefined) {
|
|
136
|
+
const globalWait = this.getGlobalReset(options.global) - now;
|
|
137
|
+
if (globalWait > 0)
|
|
138
|
+
return {
|
|
139
|
+
granted: false,
|
|
140
|
+
retryAfterMs: globalWait,
|
|
141
|
+
reason: "global"
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
const limit = options.global === undefined ? 0 : options.globalLimit ?? 0;
|
|
145
|
+
let log;
|
|
146
|
+
if (limit > 0) {
|
|
147
|
+
log = this.#globalLogs.get(options.global);
|
|
148
|
+
if (!log) {
|
|
149
|
+
this.#globalLogs.set(options.global, log = []);
|
|
150
|
+
MemoryRateLimitStore2.#evict(this.#globalLogs);
|
|
151
|
+
}
|
|
152
|
+
const wait = takeGlobalSlot(log, now, limit);
|
|
153
|
+
if (wait > 0)
|
|
154
|
+
return { granted: false, retryAfterMs: wait, reason: "global" };
|
|
155
|
+
log.pop();
|
|
156
|
+
}
|
|
157
|
+
const { reservation, next } = reserveState(this.#buckets.get(key), now, options.probeMs ?? DEFAULT_PROBE_MS);
|
|
158
|
+
if (log && reservation.granted)
|
|
159
|
+
log.push(now);
|
|
160
|
+
if (next)
|
|
161
|
+
this.#write(key, next);
|
|
162
|
+
return reservation;
|
|
163
|
+
}
|
|
164
|
+
reconcile(key, update) {
|
|
165
|
+
const next = reconcileState(this.#buckets.get(key), update, Date.now());
|
|
166
|
+
if (next === null)
|
|
167
|
+
this.#buckets.delete(key);
|
|
168
|
+
else if (next)
|
|
169
|
+
this.#write(key, next);
|
|
170
|
+
}
|
|
171
|
+
release(key, grant, refund) {
|
|
172
|
+
const state = this.#buckets.get(key);
|
|
173
|
+
if (!state || state.resetAt !== grant.resetAt)
|
|
174
|
+
return;
|
|
175
|
+
if (state.probe)
|
|
176
|
+
this.#buckets.delete(key);
|
|
177
|
+
else if (refund)
|
|
178
|
+
state.remaining = Math.min(state.remaining + 1, state.limit ?? Number.POSITIVE_INFINITY);
|
|
179
|
+
}
|
|
180
|
+
updateBucket(key, state) {
|
|
181
|
+
this.#write(key, state);
|
|
182
|
+
}
|
|
183
|
+
#write(key, state) {
|
|
184
|
+
this.#buckets.delete(key);
|
|
185
|
+
this.#buckets.set(key, state);
|
|
186
|
+
MemoryRateLimitStore2.#evict(this.#buckets);
|
|
187
|
+
const now = Date.now();
|
|
188
|
+
if (now - this.#lastPruneAt >= MemoryRateLimitStore2.#PRUNE_INTERVAL) {
|
|
189
|
+
this.#lastPruneAt = now;
|
|
190
|
+
this.prune(BUCKET_LINGER_MS);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
prune(maxAge = 0) {
|
|
194
|
+
const cutoff = Date.now() - Math.max(0, maxAge);
|
|
195
|
+
for (const [key, state] of this.#buckets) {
|
|
196
|
+
if (state.resetAt <= cutoff)
|
|
197
|
+
this.#buckets.delete(key);
|
|
198
|
+
}
|
|
199
|
+
for (const [scope, resetAt] of this.#globals)
|
|
200
|
+
if (resetAt <= Date.now())
|
|
201
|
+
this.#globals.delete(scope);
|
|
202
|
+
for (const [scope, log] of this.#globalLogs)
|
|
203
|
+
if ((log.at(-1) ?? 0) <= Date.now() - GLOBAL_WINDOW_MS)
|
|
204
|
+
this.#globalLogs.delete(scope);
|
|
205
|
+
}
|
|
206
|
+
getGlobalReset(scope = "") {
|
|
207
|
+
return this.#globals.get(scope) ?? 0;
|
|
208
|
+
}
|
|
209
|
+
setGlobalReset(resetAt, scope = "") {
|
|
210
|
+
this.#globals.set(scope, resetAt);
|
|
211
|
+
MemoryRateLimitStore2.#evict(this.#globals);
|
|
212
|
+
}
|
|
213
|
+
extendGlobalReset(scope, waitMs) {
|
|
214
|
+
const resetAt = Date.now() + Math.max(0, waitMs);
|
|
215
|
+
if (resetAt > this.getGlobalReset(scope))
|
|
216
|
+
this.setGlobalReset(resetAt, scope);
|
|
217
|
+
}
|
|
218
|
+
get size() {
|
|
219
|
+
return this.#buckets.size;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// packages/rest/src/errors.ts
|
|
224
|
+
function kindFromStatus(status) {
|
|
225
|
+
if (status === 429)
|
|
226
|
+
return "rateLimited";
|
|
227
|
+
if (status === 401)
|
|
228
|
+
return "authentication";
|
|
229
|
+
if (status === 403)
|
|
230
|
+
return "permission";
|
|
231
|
+
if (status === 404)
|
|
232
|
+
return "notFound";
|
|
233
|
+
if (status === 400 || status === 422)
|
|
234
|
+
return "validation";
|
|
235
|
+
if (status >= 500)
|
|
236
|
+
return "server";
|
|
237
|
+
if (status === 0)
|
|
238
|
+
return "network";
|
|
239
|
+
return "client";
|
|
240
|
+
}
|
|
241
|
+
function redactPath2(path) {
|
|
242
|
+
return path.replace(/^(\/webhooks\/[^/?]+\/)[^/?]+/i, "$1:token").replace(/^(\/interactions\/[^/?]+\/)[^/?]+/i, "$1:token");
|
|
243
|
+
}
|
|
244
|
+
var TOKEN_IN_TEXT = /(\/(?:webhooks|interactions)\/[^/\s?"'`]+\/)[^/\s?"'`]+/gi;
|
|
245
|
+
function redactText(text) {
|
|
246
|
+
return text.replace(TOKEN_IN_TEXT, "$1:token");
|
|
247
|
+
}
|
|
248
|
+
function sanitizeCause(error) {
|
|
249
|
+
if (!(error instanceof Error))
|
|
250
|
+
return;
|
|
251
|
+
const copy = new Error(redactText(error.message));
|
|
252
|
+
copy.name = error.name;
|
|
253
|
+
if (error.stack)
|
|
254
|
+
copy.stack = redactText(error.stack);
|
|
255
|
+
for (const key of ["code", "errno", "syscall"]) {
|
|
256
|
+
const value = error[key];
|
|
257
|
+
if (typeof value === "string" || typeof value === "number")
|
|
258
|
+
copy[key] = value;
|
|
259
|
+
}
|
|
260
|
+
return copy;
|
|
261
|
+
}
|
|
262
|
+
var HINTS = {
|
|
263
|
+
50001: "The bot cannot see this resource: check that it is in the guild and has View Channel there.",
|
|
264
|
+
50013: "The bot lacks a permission for this action, or the target role or member is at or above the bot's highest role."
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
class RESTError2 extends Error {
|
|
268
|
+
kind;
|
|
269
|
+
status;
|
|
270
|
+
code;
|
|
271
|
+
errors;
|
|
272
|
+
method;
|
|
273
|
+
path;
|
|
274
|
+
hint;
|
|
275
|
+
constructor(message, status, code, errors, options = {}) {
|
|
276
|
+
super(message, options.cause === undefined ? undefined : { cause: options.cause });
|
|
277
|
+
this.name = "RESTError";
|
|
278
|
+
this.status = status;
|
|
279
|
+
this.kind = options.kind ?? kindFromStatus(status);
|
|
280
|
+
this.code = code;
|
|
281
|
+
this.hint = code === undefined ? undefined : HINTS[code];
|
|
282
|
+
this.errors = errors;
|
|
283
|
+
this.method = options.method;
|
|
284
|
+
this.path = options.path === undefined ? undefined : redactPath2(options.path);
|
|
285
|
+
}
|
|
286
|
+
get retryable() {
|
|
287
|
+
return this.kind === "rateLimited" || this.kind === "server" || this.kind === "network" || this.kind === "timeout" || this.kind === "coordination" || this.kind === "overloaded";
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
function abortError2(path, reason) {
|
|
291
|
+
return new RESTError2(reason instanceof Error ? reason.message : "Discord REST request was aborted", 0, undefined, undefined, { path, cause: reason, kind: "aborted" });
|
|
292
|
+
}
|
|
293
|
+
var MAX_TIMER_MS = 2147483647;
|
|
294
|
+
async function sleep(delay, signal, path) {
|
|
295
|
+
if (delay <= 0)
|
|
296
|
+
return;
|
|
297
|
+
delay = Math.min(delay, MAX_TIMER_MS);
|
|
298
|
+
await new Promise((resolve, reject) => {
|
|
299
|
+
const finish = (error) => {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
signal?.removeEventListener("abort", onAbort);
|
|
302
|
+
if (error)
|
|
303
|
+
reject(error);
|
|
304
|
+
else
|
|
305
|
+
resolve();
|
|
306
|
+
};
|
|
307
|
+
const onAbort = () => finish(abortError2(path, signal?.reason));
|
|
308
|
+
const timer = setTimeout(finish, delay);
|
|
309
|
+
if (signal?.aborted)
|
|
310
|
+
return onAbort();
|
|
311
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
async function abortable(promise, signal, path) {
|
|
315
|
+
if (!signal)
|
|
316
|
+
return promise;
|
|
317
|
+
if (signal.aborted)
|
|
318
|
+
throw abortError2(path, signal.reason);
|
|
319
|
+
return new Promise((resolve, reject) => {
|
|
320
|
+
const onAbort = () => reject(abortError2(path, signal.reason));
|
|
321
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
322
|
+
promise.then(() => {
|
|
323
|
+
signal.removeEventListener("abort", onAbort);
|
|
324
|
+
resolve();
|
|
325
|
+
}, (error) => {
|
|
326
|
+
signal.removeEventListener("abort", onAbort);
|
|
327
|
+
reject(error);
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// packages/rest/src/route.ts
|
|
333
|
+
import { createHash } from "crypto";
|
|
334
|
+
function normalizeRoutePath2(path) {
|
|
335
|
+
const queryIndex = path.indexOf("?");
|
|
336
|
+
const routePath = queryIndex === -1 ? path : path.slice(0, queryIndex);
|
|
337
|
+
return redactPath2(routePath).replace(/\/\d+(?=\/|$)/g, "/:id").replace(/\/reactions\/[^/]+/i, "/reactions/:emoji");
|
|
338
|
+
}
|
|
339
|
+
function majorParameter2(path) {
|
|
340
|
+
const match = /^\/(channels|guilds|webhooks)\/(\d+)(?:\/([^/?]+))?/i.exec(path);
|
|
341
|
+
if (!match)
|
|
342
|
+
return "@none";
|
|
343
|
+
if (match[1].toLowerCase() === "webhooks" && match[3] !== undefined)
|
|
344
|
+
return `${match[2]}:${tokenDigest(match[3])}`;
|
|
345
|
+
return match[2];
|
|
346
|
+
}
|
|
347
|
+
function tokenDigest(token) {
|
|
348
|
+
return createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
349
|
+
}
|
|
350
|
+
function scopeBucket2(hashOrRoute, major) {
|
|
351
|
+
return `${hashOrRoute}|${major}`;
|
|
352
|
+
}
|
|
353
|
+
function createRouteKey2(method, path) {
|
|
354
|
+
const normalizedMethod = method.toUpperCase();
|
|
355
|
+
return {
|
|
356
|
+
method: normalizedMethod,
|
|
357
|
+
path,
|
|
358
|
+
route: `${normalizedMethod}:${normalizeRoutePath2(path)}`,
|
|
359
|
+
major: majorParameter2(path)
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
function serializeQuery2(query) {
|
|
363
|
+
if (query === undefined)
|
|
364
|
+
return "";
|
|
365
|
+
if (typeof query === "string")
|
|
366
|
+
return query.length === 0 || query.startsWith("?") ? query : `?${query}`;
|
|
367
|
+
const params = query instanceof URLSearchParams ? query : new URLSearchParams(Object.entries(query).flatMap(([key, value]) => value === undefined || value === null ? [] : [[key, String(value)]]));
|
|
368
|
+
const serialized = params.toString();
|
|
369
|
+
return serialized.length === 0 ? "" : `?${serialized}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
// packages/rest/src/scheduler.ts
|
|
373
|
+
class RequestScheduler2 {
|
|
374
|
+
static MAX_REMAPS = 2;
|
|
375
|
+
#queues = new Map;
|
|
376
|
+
#waiting = new Map;
|
|
377
|
+
#queued = 0;
|
|
378
|
+
#maxQueued;
|
|
379
|
+
#maxQueuedPerBucket;
|
|
380
|
+
constructor(options = {}) {
|
|
381
|
+
this.#maxQueued = options.maxQueued ?? Number.POSITIVE_INFINITY;
|
|
382
|
+
this.#maxQueuedPerBucket = options.maxQueuedPerBucket ?? Number.POSITIVE_INFINITY;
|
|
383
|
+
if (!(this.#maxQueued >= 0) || !(this.#maxQueuedPerBucket >= 0))
|
|
384
|
+
throw new RangeError("Queue limits must be non-negative numbers.");
|
|
385
|
+
}
|
|
386
|
+
get size() {
|
|
387
|
+
return this.#queues.size;
|
|
388
|
+
}
|
|
389
|
+
get queued() {
|
|
390
|
+
return this.#queued;
|
|
391
|
+
}
|
|
392
|
+
async runResolved(resolveKey, signal, path, task) {
|
|
393
|
+
let key = await resolveKey();
|
|
394
|
+
for (let hop = 0;hop < RequestScheduler2.MAX_REMAPS; hop++) {
|
|
395
|
+
const outcome = await this.run(key, signal, path, async (release) => {
|
|
396
|
+
const current = await resolveKey();
|
|
397
|
+
if (current !== key)
|
|
398
|
+
return { done: false, key: current };
|
|
399
|
+
return { done: true, value: await task(key, release) };
|
|
400
|
+
});
|
|
401
|
+
if (outcome.done)
|
|
402
|
+
return outcome.value;
|
|
403
|
+
key = outcome.key;
|
|
404
|
+
}
|
|
405
|
+
return this.run(key, signal, path, (release) => task(key, release));
|
|
406
|
+
}
|
|
407
|
+
async run(key, signal, path, task) {
|
|
408
|
+
const previous = this.#queues.get(key);
|
|
409
|
+
const waiting = this.#waiting.get(key) ?? 0;
|
|
410
|
+
if (previous && (this.#queued >= this.#maxQueued || waiting >= this.#maxQueuedPerBucket))
|
|
411
|
+
throw new RESTError2("The REST request queue is full; request not sent.", 0, undefined, undefined, { path, kind: "overloaded" });
|
|
412
|
+
let open;
|
|
413
|
+
const gate = new Promise((resolve) => {
|
|
414
|
+
open = resolve;
|
|
415
|
+
});
|
|
416
|
+
this.#queues.set(key, gate);
|
|
417
|
+
let released = false;
|
|
418
|
+
let started = false;
|
|
419
|
+
const release = () => {
|
|
420
|
+
if (released)
|
|
421
|
+
return;
|
|
422
|
+
released = true;
|
|
423
|
+
if (!started && previous)
|
|
424
|
+
previous.then(open, open);
|
|
425
|
+
else
|
|
426
|
+
open();
|
|
427
|
+
if (this.#queues.get(key) === gate)
|
|
428
|
+
this.#queues.delete(key);
|
|
429
|
+
};
|
|
430
|
+
try {
|
|
431
|
+
if (previous) {
|
|
432
|
+
this.#queued++;
|
|
433
|
+
this.#waiting.set(key, waiting + 1);
|
|
434
|
+
try {
|
|
435
|
+
await abortable(previous, signal, path);
|
|
436
|
+
} finally {
|
|
437
|
+
this.#queued--;
|
|
438
|
+
const left = (this.#waiting.get(key) ?? 1) - 1;
|
|
439
|
+
if (left > 0)
|
|
440
|
+
this.#waiting.set(key, left);
|
|
441
|
+
else
|
|
442
|
+
this.#waiting.delete(key);
|
|
443
|
+
}
|
|
444
|
+
} else
|
|
445
|
+
await abortable(Promise.resolve(), signal, path);
|
|
446
|
+
started = true;
|
|
447
|
+
return await task(release);
|
|
448
|
+
} finally {
|
|
449
|
+
release();
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// packages/rest/src/headers.ts
|
|
455
|
+
function headerNumber2(headers, name) {
|
|
456
|
+
const raw = headers.get(name);
|
|
457
|
+
if (raw === null || raw.trim() === "")
|
|
458
|
+
return;
|
|
459
|
+
const value = Number(raw);
|
|
460
|
+
return Number.isFinite(value) && value >= 0 ? value : undefined;
|
|
461
|
+
}
|
|
462
|
+
var BUCKET_HASH = /^[A-Za-z0-9:_\-.]{1,64}$/;
|
|
463
|
+
var MAX_COUNT = 1e5;
|
|
464
|
+
function count(value) {
|
|
465
|
+
return value === undefined || value > MAX_COUNT ? undefined : Math.floor(value);
|
|
466
|
+
}
|
|
467
|
+
function parseRateLimitHeaders2(headers) {
|
|
468
|
+
const remaining = headerNumber2(headers, "X-RateLimit-Remaining");
|
|
469
|
+
const limit = headerNumber2(headers, "X-RateLimit-Limit");
|
|
470
|
+
const resetAfter = headerNumber2(headers, "X-RateLimit-Reset-After");
|
|
471
|
+
const reset = headerNumber2(headers, "X-RateLimit-Reset");
|
|
472
|
+
const retryAfter = headerNumber2(headers, "Retry-After");
|
|
473
|
+
const bucket = headers.get("X-RateLimit-Bucket")?.trim();
|
|
474
|
+
const scope = headers.get("X-RateLimit-Scope")?.trim().toLowerCase();
|
|
475
|
+
return {
|
|
476
|
+
limit: count(limit),
|
|
477
|
+
remaining: count(remaining),
|
|
478
|
+
resetAfterMs: resetAfter === undefined ? undefined : resetAfter * 1000,
|
|
479
|
+
resetAtMs: reset === undefined ? undefined : reset * 1000,
|
|
480
|
+
bucket: bucket && BUCKET_HASH.test(bucket) ? bucket : undefined,
|
|
481
|
+
scope: scope === "user" || scope === "global" || scope === "shared" ? scope : undefined,
|
|
482
|
+
global: headers.get("X-RateLimit-Global")?.trim().toLowerCase() === "true",
|
|
483
|
+
retryAfterMs: retryAfter === undefined ? undefined : retryAfter * 1000
|
|
484
|
+
};
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// packages/rest/src/telemetry.ts
|
|
488
|
+
function emitTelemetry(sink, event) {
|
|
489
|
+
if (!sink)
|
|
490
|
+
return;
|
|
491
|
+
try {
|
|
492
|
+
sink.emit(event);
|
|
493
|
+
} catch {}
|
|
494
|
+
}
|
|
495
|
+
var LATENCY_BOUNDS = [
|
|
496
|
+
1,
|
|
497
|
+
5,
|
|
498
|
+
10,
|
|
499
|
+
25,
|
|
500
|
+
50,
|
|
501
|
+
100,
|
|
502
|
+
250,
|
|
503
|
+
500,
|
|
504
|
+
1000,
|
|
505
|
+
2500,
|
|
506
|
+
5000,
|
|
507
|
+
1e4,
|
|
508
|
+
30000,
|
|
509
|
+
60000
|
|
510
|
+
];
|
|
511
|
+
|
|
512
|
+
class RESTMetrics2 {
|
|
513
|
+
#counters = new Map;
|
|
514
|
+
#gauges = new Map;
|
|
515
|
+
#histograms = new Map;
|
|
516
|
+
#routes = new Set;
|
|
517
|
+
#maxRoutes;
|
|
518
|
+
constructor(options = {}) {
|
|
519
|
+
this.#maxRoutes = options.maxRoutes ?? 500;
|
|
520
|
+
}
|
|
521
|
+
#route(route) {
|
|
522
|
+
if (this.#routes.has(route))
|
|
523
|
+
return route;
|
|
524
|
+
if (this.#routes.size >= this.#maxRoutes)
|
|
525
|
+
return "other";
|
|
526
|
+
this.#routes.add(route);
|
|
527
|
+
return route;
|
|
528
|
+
}
|
|
529
|
+
#inc(name, labels = {}, by = 1) {
|
|
530
|
+
const key = metricKey(name, labels);
|
|
531
|
+
this.#counters.set(key, (this.#counters.get(key) ?? 0) + by);
|
|
532
|
+
}
|
|
533
|
+
#observe(name, value, labels = {}) {
|
|
534
|
+
const key = metricKey(name, labels);
|
|
535
|
+
let h = this.#histograms.get(key);
|
|
536
|
+
if (!h) {
|
|
537
|
+
h = {
|
|
538
|
+
counts: new Array(LATENCY_BOUNDS.length + 1).fill(0),
|
|
539
|
+
sum: 0,
|
|
540
|
+
count: 0
|
|
541
|
+
};
|
|
542
|
+
this.#histograms.set(key, h);
|
|
543
|
+
}
|
|
544
|
+
let i = LATENCY_BOUNDS.findIndex((bound) => value <= bound);
|
|
545
|
+
if (i === -1)
|
|
546
|
+
i = LATENCY_BOUNDS.length;
|
|
547
|
+
h.counts[i]++;
|
|
548
|
+
h.sum += value;
|
|
549
|
+
h.count++;
|
|
550
|
+
}
|
|
551
|
+
emit(event) {
|
|
552
|
+
switch (event.type) {
|
|
553
|
+
case "response":
|
|
554
|
+
this.#inc("responses_total", {
|
|
555
|
+
method: event.method,
|
|
556
|
+
route: this.#route(event.route),
|
|
557
|
+
status: String(event.status)
|
|
558
|
+
});
|
|
559
|
+
this.#observe("response_latency_ms", event.durationMs, {
|
|
560
|
+
method: event.method
|
|
561
|
+
});
|
|
562
|
+
break;
|
|
563
|
+
case "request":
|
|
564
|
+
this.#inc("requests_total", {
|
|
565
|
+
method: event.method,
|
|
566
|
+
outcome: event.ok ? "ok" : "error"
|
|
567
|
+
});
|
|
568
|
+
this.#observe("request_latency_ms", event.durationMs, {
|
|
569
|
+
method: event.method
|
|
570
|
+
});
|
|
571
|
+
break;
|
|
572
|
+
case "rateLimited":
|
|
573
|
+
this.#inc("rate_limited_total", {
|
|
574
|
+
scope: event.scope,
|
|
575
|
+
route: this.#route(event.route)
|
|
576
|
+
});
|
|
577
|
+
break;
|
|
578
|
+
case "wait":
|
|
579
|
+
this.#inc("waits_total", { reason: event.reason });
|
|
580
|
+
this.#observe("wait_ms", event.durationMs, {
|
|
581
|
+
reason: event.reason
|
|
582
|
+
});
|
|
583
|
+
break;
|
|
584
|
+
case "reservation":
|
|
585
|
+
this.#inc("reservations_total", {
|
|
586
|
+
outcome: event.granted ? "granted" : "refused"
|
|
587
|
+
});
|
|
588
|
+
this.#observe("reservation_latency_ms", event.latencyMs);
|
|
589
|
+
break;
|
|
590
|
+
case "retry":
|
|
591
|
+
this.#inc("retries_total", { reason: event.reason });
|
|
592
|
+
break;
|
|
593
|
+
case "cancelled":
|
|
594
|
+
this.#inc("cancellations_total", { stage: event.stage });
|
|
595
|
+
break;
|
|
596
|
+
case "overloaded":
|
|
597
|
+
this.#inc("overloaded_total");
|
|
598
|
+
break;
|
|
599
|
+
case "tokenRejected":
|
|
600
|
+
this.#inc("token_rejected_total");
|
|
601
|
+
break;
|
|
602
|
+
case "networkError":
|
|
603
|
+
this.#inc("network_errors_total", {
|
|
604
|
+
timeout: String(event.timedOut)
|
|
605
|
+
});
|
|
606
|
+
break;
|
|
607
|
+
case "bucket":
|
|
608
|
+
this.#inc("bucket_changes_total", { change: event.change });
|
|
609
|
+
break;
|
|
610
|
+
case "store":
|
|
611
|
+
this.#inc("store_commands_total", {
|
|
612
|
+
operation: event.operation,
|
|
613
|
+
ok: String(event.ok)
|
|
614
|
+
});
|
|
615
|
+
this.#observe("store_latency_ms", event.latencyMs, {
|
|
616
|
+
operation: event.operation
|
|
617
|
+
});
|
|
618
|
+
break;
|
|
619
|
+
case "mode":
|
|
620
|
+
this.#inc("mode_changes_total", { mode: event.mode });
|
|
621
|
+
this.#gauges.set("degraded", event.mode === "degraded-local" || event.mode === "unavailable" ? 1 : 0);
|
|
622
|
+
break;
|
|
623
|
+
case "queue":
|
|
624
|
+
this.#gauges.set("queued", event.queued);
|
|
625
|
+
this.#gauges.set("in_flight", event.inFlight);
|
|
626
|
+
break;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
snapshot() {
|
|
630
|
+
const histograms = {};
|
|
631
|
+
for (const [key, h] of this.#histograms) {
|
|
632
|
+
const buckets = {};
|
|
633
|
+
let running = 0;
|
|
634
|
+
LATENCY_BOUNDS.forEach((bound, i) => {
|
|
635
|
+
running += h.counts[i];
|
|
636
|
+
buckets[String(bound)] = running;
|
|
637
|
+
});
|
|
638
|
+
buckets["+Inf"] = h.count;
|
|
639
|
+
histograms[key] = { count: h.count, sum: h.sum, buckets };
|
|
640
|
+
}
|
|
641
|
+
return {
|
|
642
|
+
counters: Object.fromEntries(this.#counters),
|
|
643
|
+
gauges: Object.fromEntries(this.#gauges),
|
|
644
|
+
histograms
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
reset() {
|
|
648
|
+
this.#counters.clear();
|
|
649
|
+
this.#gauges.clear();
|
|
650
|
+
this.#histograms.clear();
|
|
651
|
+
this.#routes.clear();
|
|
652
|
+
}
|
|
653
|
+
toPrometheus(prefix = "lunibee_rest") {
|
|
654
|
+
const lines = [];
|
|
655
|
+
const typed = new Set;
|
|
656
|
+
const type = (name, kind) => {
|
|
657
|
+
if (typed.has(name))
|
|
658
|
+
return;
|
|
659
|
+
typed.add(name);
|
|
660
|
+
lines.push(`# TYPE ${name} ${kind}`);
|
|
661
|
+
};
|
|
662
|
+
for (const [key, value] of this.#counters) {
|
|
663
|
+
const name = `${prefix}_${key.split("{")[0]}`;
|
|
664
|
+
type(name, "counter");
|
|
665
|
+
lines.push(`${prefix}_${key} ${value}`);
|
|
666
|
+
}
|
|
667
|
+
for (const [key, value] of this.#gauges) {
|
|
668
|
+
type(`${prefix}_${key}`, "gauge");
|
|
669
|
+
lines.push(`${prefix}_${key} ${value}`);
|
|
670
|
+
}
|
|
671
|
+
for (const [key, h] of this.#histograms) {
|
|
672
|
+
const [base, rawLabels = ""] = key.split("{");
|
|
673
|
+
const labels = rawLabels.replace(/\}$/, "");
|
|
674
|
+
const name = `${prefix}_${base}`;
|
|
675
|
+
type(name, "histogram");
|
|
676
|
+
const join = (extra) => `{${[labels, extra].filter(Boolean).join(",")}}`;
|
|
677
|
+
let running = 0;
|
|
678
|
+
LATENCY_BOUNDS.forEach((bound, i) => {
|
|
679
|
+
running += h.counts[i];
|
|
680
|
+
lines.push(`${name}_bucket${join(`le="${bound}"`)} ${running}`);
|
|
681
|
+
});
|
|
682
|
+
lines.push(`${name}_bucket${join('le="+Inf"')} ${h.count}`);
|
|
683
|
+
lines.push(`${name}_sum${labels ? `{${labels}}` : ""} ${h.sum}`);
|
|
684
|
+
lines.push(`${name}_count${labels ? `{${labels}}` : ""} ${h.count}`);
|
|
685
|
+
}
|
|
686
|
+
return `${lines.join(`
|
|
687
|
+
`)}
|
|
688
|
+
`;
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
function metricKey(name, labels) {
|
|
692
|
+
const entries = Object.entries(labels);
|
|
693
|
+
if (entries.length === 0)
|
|
694
|
+
return name;
|
|
695
|
+
return `${name}{${entries.map(([k, v]) => `${k}="${v.replace(/["\\\n]/g, "_")}"`).join(",")}}`;
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// packages/rest/src/limiter.ts
|
|
699
|
+
var DEFAULT_MAX_BACKOFF_MS2 = 600000;
|
|
700
|
+
var MAX_IDLE_REFUSALS = 16;
|
|
701
|
+
var MAX_CACHED_ROUTES = 1e4;
|
|
702
|
+
|
|
703
|
+
class RateLimiter2 {
|
|
704
|
+
#store;
|
|
705
|
+
#maxBackoff;
|
|
706
|
+
#telemetry;
|
|
707
|
+
#probeMs;
|
|
708
|
+
#routes = new Map;
|
|
709
|
+
#globalLimit;
|
|
710
|
+
#localGlobal = new Map;
|
|
711
|
+
constructor(store, options = {}) {
|
|
712
|
+
this.#store = store;
|
|
713
|
+
this.#maxBackoff = options.maxBackoff ?? DEFAULT_MAX_BACKOFF_MS2;
|
|
714
|
+
if (!(this.#maxBackoff >= 0))
|
|
715
|
+
throw new RangeError("maxBackoff must be a non-negative number.");
|
|
716
|
+
this.#probeMs = options.probeMs ?? DEFAULT_PROBE_MS;
|
|
717
|
+
if (!(this.#probeMs > 0))
|
|
718
|
+
throw new RangeError("probeMs must be a positive number.");
|
|
719
|
+
this.#telemetry = options.telemetry;
|
|
720
|
+
this.#globalLimit = options.globalLimit ?? 0;
|
|
721
|
+
if (!(this.#globalLimit >= 0) || !Number.isFinite(this.#globalLimit))
|
|
722
|
+
throw new RangeError("globalLimit must be a non-negative number.");
|
|
723
|
+
}
|
|
724
|
+
get maxBackoff() {
|
|
725
|
+
return this.#maxBackoff;
|
|
726
|
+
}
|
|
727
|
+
#capWait(ms) {
|
|
728
|
+
return Math.min(Math.max(0, ms), this.#maxBackoff);
|
|
729
|
+
}
|
|
730
|
+
get store() {
|
|
731
|
+
return this.#store;
|
|
732
|
+
}
|
|
733
|
+
get reserves() {
|
|
734
|
+
return typeof this.#store.reserve === "function";
|
|
735
|
+
}
|
|
736
|
+
#cacheRoute(route, hash) {
|
|
737
|
+
this.#routes.delete(route);
|
|
738
|
+
this.#routes.set(route, hash);
|
|
739
|
+
if (this.#routes.size > MAX_CACHED_ROUTES)
|
|
740
|
+
this.#routes.delete(this.#routes.keys().next().value);
|
|
741
|
+
}
|
|
742
|
+
async resolveBucketKey(route, major, identity) {
|
|
743
|
+
let hash = this.#routes.get(`${route}\x00${major}`) ?? this.#routes.get(route);
|
|
744
|
+
if (hash === undefined) {
|
|
745
|
+
hash = await this.#store.getBucketHash(route);
|
|
746
|
+
if (hash !== undefined)
|
|
747
|
+
this.#cacheRoute(route, hash);
|
|
748
|
+
}
|
|
749
|
+
const key = scopeBucket2(hash ?? route, major);
|
|
750
|
+
return identity === undefined ? key : `${identity}/${key}`;
|
|
751
|
+
}
|
|
752
|
+
async#call(path, operation, call) {
|
|
753
|
+
try {
|
|
754
|
+
return await call();
|
|
755
|
+
} catch (error) {
|
|
756
|
+
if (error instanceof RESTError2)
|
|
757
|
+
throw error;
|
|
758
|
+
throw new RESTError2(`The rate-limit store failed (${operation}); request not sent.`, 0, undefined, undefined, { path, kind: "coordination", cause: sanitizeCause(error) });
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
async acquire(initialKey, signal, path, options = {}) {
|
|
762
|
+
const store = this.#store;
|
|
763
|
+
const global = "global" in options ? options.global : "";
|
|
764
|
+
if (!store.reserve) {
|
|
765
|
+
const bucket = await this.#call(path, "getBucket", () => store.getBucket(initialKey));
|
|
766
|
+
if (bucket) {
|
|
767
|
+
const delay = bucket.resetAt - Date.now();
|
|
768
|
+
if (bucket.remaining <= 0 && delay > 0)
|
|
769
|
+
await this.#wait("bucket", delay, signal, path);
|
|
770
|
+
}
|
|
771
|
+
await this.#waitGlobal(global ?? "", signal, path);
|
|
772
|
+
if (global !== undefined)
|
|
773
|
+
await this.#localGlobalSlot(global, signal, path);
|
|
774
|
+
return {
|
|
775
|
+
reserved: false,
|
|
776
|
+
known: !!bucket && bucket.resetAt > Date.now()
|
|
777
|
+
};
|
|
778
|
+
}
|
|
779
|
+
const started = performance.now();
|
|
780
|
+
let idle = 0;
|
|
781
|
+
let bucketKey = initialKey;
|
|
782
|
+
for (;; ) {
|
|
783
|
+
if (global !== undefined && !store.reservesGlobal) {
|
|
784
|
+
await this.#waitGlobal(global, signal, path);
|
|
785
|
+
await this.#localGlobalSlot(global, signal, path);
|
|
786
|
+
}
|
|
787
|
+
const asked = performance.now();
|
|
788
|
+
const reservation = await this.#call(path, "reserve", () => store.reserve(bucketKey, {
|
|
789
|
+
global,
|
|
790
|
+
probeMs: this.#probeMs,
|
|
791
|
+
globalLimit: this.#globalLimit || undefined
|
|
792
|
+
}));
|
|
793
|
+
if (!reservation || typeof reservation !== "object" || typeof reservation.granted !== "boolean")
|
|
794
|
+
throw new RESTError2("The rate-limit store returned an invalid reservation; request not sent.", 0, undefined, undefined, { path, kind: "coordination" });
|
|
795
|
+
if (reservation.granted) {
|
|
796
|
+
let known = reservation.known;
|
|
797
|
+
if (known === undefined) {
|
|
798
|
+
const bucket = await this.#call(path, "getBucket", () => store.getBucket(bucketKey));
|
|
799
|
+
known = !!bucket && bucket.resetAt > Date.now();
|
|
800
|
+
}
|
|
801
|
+
emitTelemetry(this.#telemetry, {
|
|
802
|
+
type: "reservation",
|
|
803
|
+
granted: true,
|
|
804
|
+
known,
|
|
805
|
+
latencyMs: performance.now() - asked
|
|
806
|
+
});
|
|
807
|
+
return {
|
|
808
|
+
reserved: true,
|
|
809
|
+
known,
|
|
810
|
+
resetAt: reservation.resetAt,
|
|
811
|
+
...bucketKey === initialKey ? {} : { key: bucketKey }
|
|
812
|
+
};
|
|
813
|
+
}
|
|
814
|
+
emitTelemetry(this.#telemetry, {
|
|
815
|
+
type: "reservation",
|
|
816
|
+
granted: false,
|
|
817
|
+
known: true,
|
|
818
|
+
latencyMs: performance.now() - asked
|
|
819
|
+
});
|
|
820
|
+
let wait = Number(reservation.retryAfterMs);
|
|
821
|
+
if (!(wait > 0)) {
|
|
822
|
+
if (++idle > MAX_IDLE_REFUSALS)
|
|
823
|
+
throw new RESTError2("The rate-limit store keeps refusing without a wait; request not sent.", 0, undefined, undefined, { path, kind: "coordination" });
|
|
824
|
+
wait = Math.min(100, 2 ** idle);
|
|
825
|
+
} else
|
|
826
|
+
idle = 0;
|
|
827
|
+
if (performance.now() - started + wait > 2 * this.#maxBackoff)
|
|
828
|
+
throw new RESTError2(`Waited for the rate limit longer than twice maxBackoff (${Math.ceil(this.#maxBackoff / 1000)}s); request not sent.`, 429, undefined, undefined, { path });
|
|
829
|
+
await this.#wait(reservation.reason ?? "bucket", wait, signal, path);
|
|
830
|
+
if (reservation.reason === "probe" && options.resolve)
|
|
831
|
+
bucketKey = await this.#call(path, "getBucketHash", options.resolve);
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
async#wait(reason, delay, signal, path) {
|
|
835
|
+
const ms = Math.min(delay, this.#maxBackoff);
|
|
836
|
+
await sleep(ms, signal, path);
|
|
837
|
+
emitTelemetry(this.#telemetry, {
|
|
838
|
+
type: "wait",
|
|
839
|
+
reason,
|
|
840
|
+
durationMs: ms
|
|
841
|
+
});
|
|
842
|
+
}
|
|
843
|
+
async#localGlobalSlot(scope, signal, path) {
|
|
844
|
+
if (!this.#globalLimit)
|
|
845
|
+
return;
|
|
846
|
+
let log = this.#localGlobal.get(scope);
|
|
847
|
+
if (!log) {
|
|
848
|
+
this.#localGlobal.set(scope, log = []);
|
|
849
|
+
if (this.#localGlobal.size > MAX_CACHED_ROUTES)
|
|
850
|
+
this.#localGlobal.delete(this.#localGlobal.keys().next().value);
|
|
851
|
+
}
|
|
852
|
+
for (;; ) {
|
|
853
|
+
const wait = takeGlobalSlot(log, Date.now(), this.#globalLimit);
|
|
854
|
+
if (wait === 0)
|
|
855
|
+
return;
|
|
856
|
+
await this.#wait("global", wait, signal, path);
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
async#waitGlobal(scope, signal, path) {
|
|
860
|
+
const globalDelay = await this.#call(path, "getGlobalReset", () => this.#store.getGlobalReset(scope)) - Date.now();
|
|
861
|
+
if (globalDelay > 0)
|
|
862
|
+
await this.#wait("global", globalDelay, signal, path);
|
|
863
|
+
}
|
|
864
|
+
async noteGlobalReset(resetAt, scope = "") {
|
|
865
|
+
const waitMs = this.#capWait(resetAt - Date.now());
|
|
866
|
+
if (this.#store.extendGlobalReset) {
|
|
867
|
+
await this.#store.extendGlobalReset(scope, waitMs);
|
|
868
|
+
return;
|
|
869
|
+
}
|
|
870
|
+
const current = await this.#store.getGlobalReset(scope);
|
|
871
|
+
const target = Date.now() + waitMs;
|
|
872
|
+
if (target > current)
|
|
873
|
+
await this.#store.setGlobalReset(target, scope);
|
|
874
|
+
}
|
|
875
|
+
retryAfterMs(response, body = {}) {
|
|
876
|
+
const fromBody = body.retry_after;
|
|
877
|
+
if (typeof fromBody === "number" && Number.isFinite(fromBody) && fromBody >= 0)
|
|
878
|
+
return fromBody * 1000;
|
|
879
|
+
const headers = parseRateLimitHeaders2(response.headers);
|
|
880
|
+
return headers.retryAfterMs ?? headers.resetAfterMs ?? 1000;
|
|
881
|
+
}
|
|
882
|
+
async applyResponse(response, bucketKey, route, major, options = {}) {
|
|
883
|
+
const headers = parseRateLimitHeaders2(response.headers);
|
|
884
|
+
let currentKey = bucketKey;
|
|
885
|
+
if (headers.bucket) {
|
|
886
|
+
const previous = this.#routes.get(route);
|
|
887
|
+
this.#cacheRoute(`${route}\x00${major}`, headers.bucket);
|
|
888
|
+
if (previous !== headers.bucket) {
|
|
889
|
+
await this.#store.setBucketHash(route, headers.bucket);
|
|
890
|
+
this.#cacheRoute(route, headers.bucket);
|
|
891
|
+
emitTelemetry(this.#telemetry, {
|
|
892
|
+
type: "bucket",
|
|
893
|
+
change: previous === undefined ? "discovered" : "remapped",
|
|
894
|
+
route
|
|
895
|
+
});
|
|
896
|
+
}
|
|
897
|
+
const key = scopeBucket2(headers.bucket, major);
|
|
898
|
+
currentKey = options.identity === undefined ? key : `${options.identity}/${key}`;
|
|
899
|
+
}
|
|
900
|
+
const limited = response.status === 429;
|
|
901
|
+
const retryAfterMs = limited ? this.#capWait(this.retryAfterMs(response, options.body)) : undefined;
|
|
902
|
+
const global = limited && (options.body?.global === true || headers.global || headers.scope === "global");
|
|
903
|
+
const onGlobal = global && options.global !== undefined;
|
|
904
|
+
let resetAfterMs = headers.resetAfterMs;
|
|
905
|
+
if (resetAfterMs === undefined && headers.resetAtMs !== undefined)
|
|
906
|
+
resetAfterMs = Math.max(0, headers.resetAtMs - Date.now());
|
|
907
|
+
const update = {
|
|
908
|
+
remaining: headers.remaining,
|
|
909
|
+
resetAfterMs: resetAfterMs === undefined ? undefined : this.#capWait(resetAfterMs),
|
|
910
|
+
limit: headers.limit,
|
|
911
|
+
window: headers.resetAtMs !== undefined && Math.abs(headers.resetAtMs - Date.now()) <= this.#maxBackoff ? headers.resetAtMs : undefined,
|
|
912
|
+
retryAfterMs: limited && !onGlobal ? retryAfterMs : undefined
|
|
913
|
+
};
|
|
914
|
+
if (this.#store.reconcile)
|
|
915
|
+
await this.#store.reconcile(currentKey, update);
|
|
916
|
+
else {
|
|
917
|
+
const next = reconcileState(await this.#store.getBucket(currentKey), update, Date.now());
|
|
918
|
+
if (next === null)
|
|
919
|
+
await this.#store.updateBucket(currentKey, {
|
|
920
|
+
remaining: 0,
|
|
921
|
+
resetAt: 0
|
|
922
|
+
});
|
|
923
|
+
else if (next)
|
|
924
|
+
await this.#store.updateBucket(currentKey, next);
|
|
925
|
+
}
|
|
926
|
+
if (currentKey !== bucketKey && options.grant?.reserved && this.#store.release)
|
|
927
|
+
await this.#store.release(bucketKey, options.grant, false);
|
|
928
|
+
if (onGlobal)
|
|
929
|
+
await this.noteGlobalReset(Date.now() + retryAfterMs, options.global);
|
|
930
|
+
return currentKey;
|
|
931
|
+
}
|
|
932
|
+
async releaseGrant(bucketKey, grant, notSent) {
|
|
933
|
+
if (!grant.reserved || !this.#store.release)
|
|
934
|
+
return;
|
|
935
|
+
try {
|
|
936
|
+
await this.#store.release(bucketKey, grant, notSent);
|
|
937
|
+
} catch {}
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
|
|
941
|
+
// packages/rest/src/identity.ts
|
|
942
|
+
import { createHash as createHash2 } from "crypto";
|
|
943
|
+
function tokenFingerprint2(token) {
|
|
944
|
+
return createHash2("sha256").update(token).digest("hex").slice(0, 16);
|
|
945
|
+
}
|
|
946
|
+
function requestIdentity2(path, fingerprint) {
|
|
947
|
+
if (/^\/interactions\//i.test(path))
|
|
948
|
+
return { bucket: "shared" };
|
|
949
|
+
if (/^\/webhooks\/[^/?]+\/[^/?]+/i.test(path))
|
|
950
|
+
return { bucket: "shared", global: fingerprint ?? "anon" };
|
|
951
|
+
if (!fingerprint)
|
|
952
|
+
return { bucket: "anon", global: "anon" };
|
|
953
|
+
return { bucket: fingerprint, global: fingerprint };
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
// packages/rest/src/transport.ts
|
|
957
|
+
var LOOPBACK = /^(?:localhost|127(?:\.\d{1,3}){3}|\[::1\])$/i;
|
|
958
|
+
function parseBaseURL(raw, allowInsecure = false) {
|
|
959
|
+
let url;
|
|
960
|
+
try {
|
|
961
|
+
url = new URL(raw);
|
|
962
|
+
} catch {
|
|
963
|
+
throw new TypeError("baseURL must be an absolute http(s) URL.");
|
|
964
|
+
}
|
|
965
|
+
if (url.protocol !== "https:" && url.protocol !== "http:")
|
|
966
|
+
throw new TypeError("baseURL must use https.");
|
|
967
|
+
if (url.username || url.password)
|
|
968
|
+
throw new TypeError("baseURL must not contain credentials.");
|
|
969
|
+
if (url.search || url.hash || /[?#]/.test(raw))
|
|
970
|
+
throw new TypeError("baseURL must not contain a query or fragment.");
|
|
971
|
+
if (url.protocol === "http:" && !allowInsecure && !LOOPBACK.test(url.hostname))
|
|
972
|
+
throw new TypeError("baseURL must use https: the bot token would travel in cleartext (allowInsecureBaseURL: true overrides this for a trusted network).");
|
|
973
|
+
return url.href.replace(/\/$/, "");
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
class TransportError2 extends Error {
|
|
977
|
+
timedOut;
|
|
978
|
+
constructor(message, timedOut, cause) {
|
|
979
|
+
super(message, cause === undefined ? undefined : { cause });
|
|
980
|
+
this.name = "TransportError";
|
|
981
|
+
this.timedOut = timedOut;
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
class HttpTransport2 {
|
|
986
|
+
#baseURL;
|
|
987
|
+
#timeout;
|
|
988
|
+
#fetch;
|
|
989
|
+
constructor(options = {}) {
|
|
990
|
+
this.#baseURL = parseBaseURL(options.baseURL ?? "https://discord.com/api/v10", options.allowInsecureBaseURL);
|
|
991
|
+
this.#timeout = Math.max(1, options.timeout ?? 15000);
|
|
992
|
+
this.#fetch = options.fetch ?? ((input, init) => globalThis.fetch(input, init));
|
|
993
|
+
}
|
|
994
|
+
get baseURL() {
|
|
995
|
+
return this.#baseURL;
|
|
996
|
+
}
|
|
997
|
+
get timeout() {
|
|
998
|
+
return this.#timeout;
|
|
999
|
+
}
|
|
1000
|
+
async send(request) {
|
|
1001
|
+
if (request.signal?.aborted)
|
|
1002
|
+
throw abortError2(request.path, request.signal.reason);
|
|
1003
|
+
const controller = new AbortController;
|
|
1004
|
+
const onAbort = () => controller.abort(request.signal?.reason);
|
|
1005
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
1006
|
+
const timer = setTimeout(() => controller.abort(new DOMException("REST request timeout", "TimeoutError")), this.#timeout);
|
|
1007
|
+
try {
|
|
1008
|
+
return await this.#fetch(`${this.#baseURL}${request.path}`, {
|
|
1009
|
+
method: request.method,
|
|
1010
|
+
headers: request.headers,
|
|
1011
|
+
body: request.body,
|
|
1012
|
+
signal: controller.signal,
|
|
1013
|
+
redirect: "error"
|
|
1014
|
+
});
|
|
1015
|
+
} catch (error) {
|
|
1016
|
+
if (error instanceof RESTError2)
|
|
1017
|
+
throw error;
|
|
1018
|
+
const timedOut = error instanceof DOMException && (error.name === "TimeoutError" || error.name === "AbortError");
|
|
1019
|
+
throw new TransportError2(timedOut ? `Discord REST request timed out after ${this.#timeout}ms` : "Discord REST request failed", timedOut, sanitizeCause(error));
|
|
1020
|
+
} finally {
|
|
1021
|
+
clearTimeout(timer);
|
|
1022
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
1023
|
+
}
|
|
1024
|
+
}
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
// packages/rest/src/decoder.ts
|
|
1028
|
+
class ResponseDecoder2 {
|
|
1029
|
+
constructor() {}
|
|
1030
|
+
async read(response) {
|
|
1031
|
+
if (response.status === 204)
|
|
1032
|
+
return;
|
|
1033
|
+
const contentType = response.headers.get("content-type") ?? "";
|
|
1034
|
+
if (contentType.includes("application/json"))
|
|
1035
|
+
return response.json().catch(() => {
|
|
1036
|
+
return;
|
|
1037
|
+
});
|
|
1038
|
+
return response.text().catch(() => {
|
|
1039
|
+
return;
|
|
1040
|
+
});
|
|
1041
|
+
}
|
|
1042
|
+
errorData(payload) {
|
|
1043
|
+
if (!payload || typeof payload !== "object")
|
|
1044
|
+
return {};
|
|
1045
|
+
const data = payload;
|
|
1046
|
+
return {
|
|
1047
|
+
message: typeof data.message === "string" ? data.message : undefined,
|
|
1048
|
+
code: typeof data.code === "number" ? data.code : undefined,
|
|
1049
|
+
retry_after: typeof data.retry_after === "number" ? data.retry_after : undefined,
|
|
1050
|
+
global: data.global === true
|
|
1051
|
+
};
|
|
1052
|
+
}
|
|
1053
|
+
retryAfter(response, data) {
|
|
1054
|
+
const raw = response.headers.get("Retry-After");
|
|
1055
|
+
const header = raw === null ? Number.NaN : Number(raw);
|
|
1056
|
+
const retryAfter = data.retry_after ?? (Number.isFinite(header) ? header : 1);
|
|
1057
|
+
return Math.max(0, retryAfter);
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
1060
|
+
|
|
1061
|
+
// packages/rest/src/routes.ts
|
|
1062
|
+
var Routes2 = {
|
|
1063
|
+
user: () => "/users/@me",
|
|
1064
|
+
userById: (userId) => `/users/${snowflake(userId, "User ID")}`,
|
|
1065
|
+
guild: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}`,
|
|
1066
|
+
currentUserGuild: (guildId) => `/users/@me/guilds/${snowflake(guildId, "Guild ID")}`,
|
|
1067
|
+
sticker: (stickerId) => `/stickers/${snowflake(stickerId, "Sticker ID")}`,
|
|
1068
|
+
messageReactionUser: (channelId, messageId, emoji, userId) => `${Routes2.messageReactions(channelId, messageId, emoji)}/${snowflake(userId, "User ID")}`,
|
|
1069
|
+
guildPreview: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/preview`,
|
|
1070
|
+
guildActiveThreads: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/threads/active`,
|
|
1071
|
+
guildWebhooks: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/webhooks`,
|
|
1072
|
+
guildInvites: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/invites`,
|
|
1073
|
+
guildChannels: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/channels`,
|
|
1074
|
+
guildMember: (guildId, userId) => `/guilds/${snowflake(guildId, "Guild ID")}/members/${snowflake(userId, "User ID")}`,
|
|
1075
|
+
guildRoles: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/roles`,
|
|
1076
|
+
guildRole: (guildId, roleId) => `/guilds/${snowflake(guildId, "Guild ID")}/roles/${snowflake(roleId, "Role ID")}`,
|
|
1077
|
+
guildMemberRole: (guildId, userId, roleId) => `/guilds/${snowflake(guildId, "Guild ID")}/members/${snowflake(userId, "User ID")}/roles/${snowflake(roleId, "Role ID")}`,
|
|
1078
|
+
guildBans: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/bans`,
|
|
1079
|
+
guildBan: (guildId, userId) => `/guilds/${snowflake(guildId, "Guild ID")}/bans/${snowflake(userId, "User ID")}`,
|
|
1080
|
+
channel: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}`,
|
|
1081
|
+
channelMessages: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/messages`,
|
|
1082
|
+
message: (channelId, messageId) => `/channels/${snowflake(channelId, "Channel ID")}/messages/${snowflake(messageId, "Message ID")}`,
|
|
1083
|
+
crosspostMessage: (channelId, messageId) => `${Routes2.message(channelId, messageId)}/crosspost`,
|
|
1084
|
+
messageReactions: (channelId, messageId, emoji) => `${Routes2.message(channelId, messageId)}/reactions/${encodeURIComponent(emoji)}`,
|
|
1085
|
+
messageReactionMe: (channelId, messageId, emoji) => `${Routes2.messageReactions(channelId, messageId, emoji)}/@me`,
|
|
1086
|
+
messageReactionsAll: (channelId, messageId) => `${Routes2.message(channelId, messageId)}/reactions`,
|
|
1087
|
+
channelMessagesPins: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/messages/pins`,
|
|
1088
|
+
channelMessagesPin: (channelId, messageId) => `/channels/${snowflake(channelId, "Channel ID")}/messages/pins/${snowflake(messageId, "Message ID")}`,
|
|
1089
|
+
messageThread: (channelId, messageId) => `${Routes2.message(channelId, messageId)}/threads`,
|
|
1090
|
+
channelBulkDelete: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/messages/bulk-delete`,
|
|
1091
|
+
channelWebhooks: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/webhooks`,
|
|
1092
|
+
channelInvites: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/invites`,
|
|
1093
|
+
webhook: (webhookId, token) => `/webhooks/${snowflake(webhookId, "Webhook ID")}${token ? `/${encodeURIComponent(token)}` : ""}`,
|
|
1094
|
+
webhookMessage: (webhookId, token, messageId) => `/webhooks/${snowflake(webhookId, "Webhook ID")}/${encodeURIComponent(token)}/messages/${snowflake(messageId, "Message ID")}`,
|
|
1095
|
+
applicationCommands: (applicationId) => `/applications/${snowflake(applicationId, "Application ID")}/commands`,
|
|
1096
|
+
applicationCommand: (applicationId, commandId) => `/applications/${snowflake(applicationId, "Application ID")}/commands/${snowflake(commandId, "Command ID")}`,
|
|
1097
|
+
guildApplicationCommands: (applicationId, guildId) => `/applications/${snowflake(applicationId, "Application ID")}/guilds/${snowflake(guildId, "Guild ID")}/commands`,
|
|
1098
|
+
guildApplicationCommand: (applicationId, guildId, commandId) => `/applications/${snowflake(applicationId, "Application ID")}/guilds/${snowflake(guildId, "Guild ID")}/commands/${snowflake(commandId, "Command ID")}`,
|
|
1099
|
+
interactionCallback: (interactionId, token) => `/interactions/${snowflake(interactionId, "Interaction ID")}/${encodeURIComponent(token)}/callback`,
|
|
1100
|
+
interactionOriginalResponse: (applicationId, token) => `/webhooks/${snowflake(applicationId, "Application ID")}/${encodeURIComponent(token)}/messages/@original`,
|
|
1101
|
+
channelPublicArchivedThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/threads/archived/public`,
|
|
1102
|
+
channelPrivateArchivedThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/threads/archived/private`,
|
|
1103
|
+
channelJoinedPrivateArchivedThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/users/@me/threads/archived/private`,
|
|
1104
|
+
threadMember: (threadId, userId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members/${userOrMe(userId)}`,
|
|
1105
|
+
channelTyping: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/typing`,
|
|
1106
|
+
channelThreads: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/threads`,
|
|
1107
|
+
threadMembers: (threadId) => `/channels/${snowflake(threadId, "Thread ID")}/thread-members`,
|
|
1108
|
+
guildScheduledEvents: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events`,
|
|
1109
|
+
guildScheduledEvent: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}`,
|
|
1110
|
+
guildScheduledEventUsers: (guildId, eventId) => `/guilds/${snowflake(guildId, "Guild ID")}/scheduled-events/${snowflake(eventId, "Event ID")}/users`,
|
|
1111
|
+
channelPermission: (channelId, overwriteId) => `/channels/${snowflake(channelId, "Channel ID")}/permissions/${snowflake(overwriteId, "Overwrite ID")}`,
|
|
1112
|
+
guildAutoModerationRules: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules`,
|
|
1113
|
+
guildAutoModerationRule: (guildId, ruleId) => `/guilds/${snowflake(guildId, "Guild ID")}/auto-moderation/rules/${snowflake(ruleId, "Rule ID")}`,
|
|
1114
|
+
voiceRegions: () => "/voice/regions",
|
|
1115
|
+
gateway: () => "/gateway",
|
|
1116
|
+
gatewayBot: () => "/gateway/bot",
|
|
1117
|
+
guildAuditLog: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/audit-logs`,
|
|
1118
|
+
guildMembers: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/members`,
|
|
1119
|
+
guildMembersSearch: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/members/search`,
|
|
1120
|
+
guildMemberMe: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/members/@me`,
|
|
1121
|
+
guildBulkBan: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/bulk-ban`,
|
|
1122
|
+
guildVoiceState: (guildId, userId) => `/guilds/${snowflake(guildId, "Guild ID")}/voice-states/${userOrMe(userId)}`,
|
|
1123
|
+
guildIntegration: (guildId, integrationId) => `/guilds/${snowflake(guildId, "Guild ID")}/integrations/${snowflake(integrationId, "Integration ID")}`,
|
|
1124
|
+
userGuilds: () => "/users/@me/guilds",
|
|
1125
|
+
stickerPacks: (packId) => packId ? `/sticker-packs/${snowflake(packId, "Sticker pack ID")}` : "/sticker-packs",
|
|
1126
|
+
guildWelcomeScreen: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/welcome-screen`,
|
|
1127
|
+
guildOnboarding: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/onboarding`,
|
|
1128
|
+
guildPrune: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/prune`,
|
|
1129
|
+
stageInstances: () => "/stage-instances",
|
|
1130
|
+
stageInstanceByChannel: (channelId) => `/stage-instances/${snowflake(channelId, "Channel ID")}`,
|
|
1131
|
+
invite: (code) => `/invites/${encodeURIComponent(code)}`,
|
|
1132
|
+
guildVanity: (guildId) => `/guilds/${snowflake(guildId, "Guild ID")}/vanity-url`,
|
|
1133
|
+
guildEmoji: (guildId, emojiId) => emojiId ? `/guilds/${snowflake(guildId, "Guild ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/emojis`,
|
|
1134
|
+
applicationEmoji: (applicationId, emojiId) => emojiId ? `/applications/${snowflake(applicationId, "Application ID")}/emojis/${snowflake(emojiId, "Emoji ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/emojis`,
|
|
1135
|
+
pollExpire: (channelId, messageId) => `/channels/${snowflake(channelId, "Channel ID")}/polls/${snowflake(messageId, "Message ID")}/expire`,
|
|
1136
|
+
pollAnswerVoters: (channelId, messageId, answerId) => `/channels/${snowflake(channelId, "Channel ID")}/polls/${snowflake(messageId, "Message ID")}/answers/${integer(answerId, "Answer ID")}`,
|
|
1137
|
+
guildSticker: (guildId, stickerId) => stickerId ? `/guilds/${snowflake(guildId, "Guild ID")}/stickers/${snowflake(stickerId, "Sticker ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/stickers`,
|
|
1138
|
+
sendSoundboardSound: (channelId) => `/channels/${snowflake(channelId, "Channel ID")}/send-soundboard-sound`,
|
|
1139
|
+
soundboardDefaultSounds: () => "/soundboard-default-sounds",
|
|
1140
|
+
guildSoundboardSound: (guildId, soundId) => soundId ? `/guilds/${snowflake(guildId, "Guild ID")}/soundboard-sounds/${snowflake(soundId, "Sound ID")}` : `/guilds/${snowflake(guildId, "Guild ID")}/soundboard-sounds`,
|
|
1141
|
+
applicationSkus: (applicationId) => `/applications/${snowflake(applicationId, "Application ID")}/skus`,
|
|
1142
|
+
applicationEntitlement: (applicationId, entitlementId) => entitlementId ? `/applications/${snowflake(applicationId, "Application ID")}/entitlements/${snowflake(entitlementId, "Entitlement ID")}` : `/applications/${snowflake(applicationId, "Application ID")}/entitlements`,
|
|
1143
|
+
consumeEntitlement: (applicationId, entitlementId) => `/applications/${snowflake(applicationId, "Application ID")}/entitlements/${snowflake(entitlementId, "Entitlement ID")}/consume`,
|
|
1144
|
+
skuSubscription: (skuId, subscriptionId) => subscriptionId ? `/skus/${snowflake(skuId, "SKU ID")}/subscriptions/${snowflake(subscriptionId, "Subscription ID")}` : `/skus/${snowflake(skuId, "SKU ID")}/subscriptions`
|
|
1145
|
+
};
|
|
1146
|
+
function userOrMe(value) {
|
|
1147
|
+
return value === "@me" ? value : snowflake(value, "User ID");
|
|
1148
|
+
}
|
|
1149
|
+
function integer(value, field) {
|
|
1150
|
+
if (!Number.isInteger(value) || value < 0)
|
|
1151
|
+
throw new TypeError(`${field} must be a non-negative integer.`);
|
|
1152
|
+
return String(value);
|
|
1153
|
+
}
|
|
1154
|
+
function snowflake(value, field) {
|
|
1155
|
+
if (!/^\d{1,20}$/.test(value))
|
|
1156
|
+
throw new TypeError(`${field} must be a valid Discord snowflake.`);
|
|
1157
|
+
return value;
|
|
1158
|
+
}
|
|
1159
|
+
// packages/rest/src/error-codes.ts
|
|
1160
|
+
var RESTErrorCode2 = {
|
|
1161
|
+
UnknownAccount: 10001,
|
|
1162
|
+
UnknownChannel: 10003,
|
|
1163
|
+
UnknownGuild: 10004,
|
|
1164
|
+
UnknownInvite: 10006,
|
|
1165
|
+
UnknownMember: 10007,
|
|
1166
|
+
UnknownMessage: 10008,
|
|
1167
|
+
UnknownPermissionOverwrite: 10009,
|
|
1168
|
+
UnknownRole: 10011,
|
|
1169
|
+
UnknownUser: 10013,
|
|
1170
|
+
UnknownEmoji: 10014,
|
|
1171
|
+
UnknownWebhook: 10015,
|
|
1172
|
+
UnknownBan: 10026,
|
|
1173
|
+
UnknownSticker: 10060,
|
|
1174
|
+
UnknownInteraction: 10062,
|
|
1175
|
+
UnknownScheduledEvent: 10070,
|
|
1176
|
+
BotsCannotUseEndpoint: 20001,
|
|
1177
|
+
MaxGuilds: 30001,
|
|
1178
|
+
MaxPins: 30003,
|
|
1179
|
+
MaxRoles: 30005,
|
|
1180
|
+
MaxWebhooks: 30007,
|
|
1181
|
+
MaxEmojis: 30008,
|
|
1182
|
+
MaxReactions: 30010,
|
|
1183
|
+
MaxChannels: 30013,
|
|
1184
|
+
InteractionAlreadyAcknowledged: 40060,
|
|
1185
|
+
MissingAccess: 50001,
|
|
1186
|
+
CannotExecuteInDM: 50003,
|
|
1187
|
+
CannotEditOthersMessage: 50005,
|
|
1188
|
+
CannotSendEmptyMessage: 50006,
|
|
1189
|
+
CannotSendMessagesToUser: 50007,
|
|
1190
|
+
MissingPermissions: 50013,
|
|
1191
|
+
InvalidToken: 50014,
|
|
1192
|
+
MessageTooOldToBulkDelete: 50034,
|
|
1193
|
+
InvalidFormBody: 50035,
|
|
1194
|
+
ThreadArchived: 50083,
|
|
1195
|
+
CannotReplyWithoutReadHistory: 160002
|
|
1196
|
+
};
|
|
1197
|
+
// packages/rest/src/redis.ts
|
|
1198
|
+
var LUA_PRELUDE = `
|
|
1199
|
+
local function now_ms()
|
|
1200
|
+
local t = redis.call('TIME')
|
|
1201
|
+
return tonumber(t[1]) * 1000 + math.floor(tonumber(t[2]) / 1000)
|
|
1202
|
+
end
|
|
1203
|
+
local function load(key)
|
|
1204
|
+
local raw = redis.call('GET', key)
|
|
1205
|
+
if not raw then return nil end
|
|
1206
|
+
local ok, s = pcall(cjson.decode, raw)
|
|
1207
|
+
if ok and type(s) == 'table' and type(s.resetAt) == 'number'
|
|
1208
|
+
and type(s.remaining) == 'number' then return s end
|
|
1209
|
+
return nil
|
|
1210
|
+
end
|
|
1211
|
+
local function save(key, s, now, linger)
|
|
1212
|
+
local ttl = math.max(1, math.ceil(s.resetAt - now)) + linger
|
|
1213
|
+
redis.call('SET', key, cjson.encode(s), 'PX', ttl)
|
|
1214
|
+
end
|
|
1215
|
+
`;
|
|
1216
|
+
var RESERVE_SCRIPT = `${LUA_PRELUDE}
|
|
1217
|
+
local now = now_ms()
|
|
1218
|
+
local glimit, gwindow = tonumber(ARGV[4]) or 0, tonumber(ARGV[5]) or 1000
|
|
1219
|
+
if #KEYS > 1 then
|
|
1220
|
+
local g = tonumber(redis.call('GET', KEYS[2]) or '0') or 0
|
|
1221
|
+
if g > now then return {0, math.ceil(g - now), 'global'} end
|
|
1222
|
+
end
|
|
1223
|
+
if #KEYS < 3 then glimit = 0 end
|
|
1224
|
+
-- A sliding log of send times: at most glimit in any gwindow span.
|
|
1225
|
+
if glimit > 0 then
|
|
1226
|
+
redis.call('ZREMRANGEBYSCORE', KEYS[3], '-inf', now - gwindow)
|
|
1227
|
+
if redis.call('ZCARD', KEYS[3]) >= glimit then
|
|
1228
|
+
local oldest = redis.call('ZRANGE', KEYS[3], 0, 0, 'WITHSCORES')
|
|
1229
|
+
return {0, math.max(1, tonumber(oldest[2]) + gwindow - now + 1), 'global'}
|
|
1230
|
+
end
|
|
1231
|
+
end
|
|
1232
|
+
local function count()
|
|
1233
|
+
if glimit > 0 then
|
|
1234
|
+
local seq = redis.call('ZCARD', KEYS[3])
|
|
1235
|
+
redis.call('ZADD', KEYS[3], now, now .. ':' .. seq)
|
|
1236
|
+
redis.call('PEXPIRE', KEYS[3], gwindow + 1000)
|
|
1237
|
+
end
|
|
1238
|
+
end
|
|
1239
|
+
local probe, poll, linger = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
|
|
1240
|
+
local s = load(KEYS[1])
|
|
1241
|
+
if s and s.resetAt > now then
|
|
1242
|
+
if s.remaining > 0 then
|
|
1243
|
+
s.remaining = s.remaining - 1
|
|
1244
|
+
save(KEYS[1], s, now, linger)
|
|
1245
|
+
count()
|
|
1246
|
+
return {1, s.resetAt, 1}
|
|
1247
|
+
end
|
|
1248
|
+
local wait = s.resetAt - now
|
|
1249
|
+
if s.provisional then wait = math.min(wait, poll) end
|
|
1250
|
+
local reason = 'bucket'
|
|
1251
|
+
if s.probe then reason = 'probe' end
|
|
1252
|
+
return {0, math.ceil(wait), reason}
|
|
1253
|
+
end
|
|
1254
|
+
local resetAt = now + probe
|
|
1255
|
+
if s and type(s.limit) == 'number' and s.limit > 0 then
|
|
1256
|
+
save(KEYS[1], {remaining = s.limit - 1, resetAt = resetAt, limit = s.limit, provisional = true}, now, linger)
|
|
1257
|
+
count()
|
|
1258
|
+
return {1, resetAt, 1}
|
|
1259
|
+
end
|
|
1260
|
+
save(KEYS[1], {remaining = 0, resetAt = resetAt, provisional = true, probe = true}, now, 0)
|
|
1261
|
+
count()
|
|
1262
|
+
return {1, resetAt, 0}
|
|
1263
|
+
`;
|
|
1264
|
+
var RECONCILE_SCRIPT = `${LUA_PRELUDE}
|
|
1265
|
+
local now = now_ms()
|
|
1266
|
+
local rem, after, limit = tonumber(ARGV[1]), tonumber(ARGV[2]), tonumber(ARGV[3])
|
|
1267
|
+
local win, retry, linger = tonumber(ARGV[4]), tonumber(ARGV[5]), tonumber(ARGV[6])
|
|
1268
|
+
local tol = 50
|
|
1269
|
+
local incoming = nil
|
|
1270
|
+
if after then incoming = math.ceil(now + after) end
|
|
1271
|
+
local s = load(KEYS[1])
|
|
1272
|
+
local n
|
|
1273
|
+
if s and s.resetAt > now and not s.probe then
|
|
1274
|
+
n = s
|
|
1275
|
+
local stale = win and n.window and win < n.window - tol
|
|
1276
|
+
if not stale then
|
|
1277
|
+
if rem then n.remaining = math.min(n.remaining, rem) end
|
|
1278
|
+
if incoming then
|
|
1279
|
+
local newer = win and n.window and win > n.window + tol
|
|
1280
|
+
if n.provisional or newer then n.resetAt = incoming
|
|
1281
|
+
else n.resetAt = math.max(n.resetAt, incoming) end
|
|
1282
|
+
n.provisional = nil
|
|
1283
|
+
end
|
|
1284
|
+
if win then n.window = math.max(n.window or 0, win) end
|
|
1285
|
+
end
|
|
1286
|
+
else
|
|
1287
|
+
if not rem and not retry then
|
|
1288
|
+
if s and s.probe then redis.call('DEL', KEYS[1]) end
|
|
1289
|
+
return 0
|
|
1290
|
+
end
|
|
1291
|
+
n = {remaining = rem or 0, resetAt = incoming or now}
|
|
1292
|
+
if s and type(s.limit) == 'number' then n.limit = s.limit end
|
|
1293
|
+
if win then n.window = win end
|
|
1294
|
+
end
|
|
1295
|
+
if limit and limit > 0 then n.limit = limit end
|
|
1296
|
+
if retry then
|
|
1297
|
+
n.remaining = 0
|
|
1298
|
+
n.resetAt = math.max(n.resetAt, math.ceil(now + retry))
|
|
1299
|
+
n.provisional = nil
|
|
1300
|
+
end
|
|
1301
|
+
save(KEYS[1], n, now, linger)
|
|
1302
|
+
return 1
|
|
1303
|
+
`;
|
|
1304
|
+
var EXTEND_GLOBAL_SCRIPT = `${LUA_PRELUDE}
|
|
1305
|
+
local now = now_ms()
|
|
1306
|
+
local target = math.ceil(now + tonumber(ARGV[1]))
|
|
1307
|
+
local cur = tonumber(redis.call('GET', KEYS[1]) or '0') or 0
|
|
1308
|
+
if target > cur then
|
|
1309
|
+
redis.call('SET', KEYS[1], tostring(target), 'PX', math.max(1, target - now))
|
|
1310
|
+
return target - now
|
|
1311
|
+
end
|
|
1312
|
+
return math.max(0, cur - now)
|
|
1313
|
+
`;
|
|
1314
|
+
var RELEASE_SCRIPT = `${LUA_PRELUDE}
|
|
1315
|
+
local s = load(KEYS[1])
|
|
1316
|
+
if not s or s.resetAt ~= tonumber(ARGV[1]) then return 0 end
|
|
1317
|
+
if s.probe then
|
|
1318
|
+
redis.call('DEL', KEYS[1])
|
|
1319
|
+
return 1
|
|
1320
|
+
end
|
|
1321
|
+
if ARGV[2] == '1' then
|
|
1322
|
+
local cap = s.remaining + 1
|
|
1323
|
+
if type(s.limit) == 'number' then cap = math.min(cap, s.limit) end
|
|
1324
|
+
s.remaining = cap
|
|
1325
|
+
redis.call('SET', KEYS[1], cjson.encode(s), 'KEEPTTL')
|
|
1326
|
+
return 1
|
|
1327
|
+
end
|
|
1328
|
+
return 0
|
|
1329
|
+
`;
|
|
1330
|
+
var VERIFY_SCRIPT = "return redis.call('TIME')[1]";
|
|
1331
|
+
function fromBunRedis2(client) {
|
|
1332
|
+
return {
|
|
1333
|
+
get: (key) => client.send("GET", [key]),
|
|
1334
|
+
mget: (keys) => client.send("MGET", keys),
|
|
1335
|
+
set: (key, value, mode, duration) => client.send("SET", mode === undefined ? [key, String(value)] : [key, String(value), mode, String(duration)]),
|
|
1336
|
+
del: (...keys) => client.send("DEL", keys),
|
|
1337
|
+
eval: (script, numKeys, ...args) => client.send("EVAL", [script, String(numKeys), ...args.map(String)])
|
|
1338
|
+
};
|
|
1339
|
+
}
|
|
1340
|
+
var SCHEMA = "v1";
|
|
1341
|
+
|
|
1342
|
+
class RedisRateLimitStore2 {
|
|
1343
|
+
#client;
|
|
1344
|
+
#prefix;
|
|
1345
|
+
#onErrorCallback;
|
|
1346
|
+
#policy;
|
|
1347
|
+
#timeout;
|
|
1348
|
+
#telemetry;
|
|
1349
|
+
#atomic;
|
|
1350
|
+
#local = new MemoryRateLimitStore2;
|
|
1351
|
+
#lastError;
|
|
1352
|
+
#warnedSlots = false;
|
|
1353
|
+
#lastFailureAt;
|
|
1354
|
+
#failures = 0;
|
|
1355
|
+
#mode;
|
|
1356
|
+
constructor(options) {
|
|
1357
|
+
this.#client = options.client;
|
|
1358
|
+
this.#prefix = `${options.prefix ?? "lunibee:rest:"}${SCHEMA}:`;
|
|
1359
|
+
this.#onErrorCallback = options.onError;
|
|
1360
|
+
this.#policy = options.failurePolicy ?? "degraded-local";
|
|
1361
|
+
if (this.#policy !== "degraded-local" && this.#policy !== "fail-closed")
|
|
1362
|
+
throw new TypeError('failurePolicy must be "degraded-local" or "fail-closed".');
|
|
1363
|
+
this.#timeout = options.commandTimeout ?? 2000;
|
|
1364
|
+
if (!(this.#timeout > 0))
|
|
1365
|
+
throw new RangeError("commandTimeout must be a positive number.");
|
|
1366
|
+
this.#telemetry = options.telemetry;
|
|
1367
|
+
this.#atomic = typeof options.client.eval === "function";
|
|
1368
|
+
if (options.requireAtomic && !this.#atomic)
|
|
1369
|
+
throw new TypeError("requireAtomic: the Redis client has no eval method, so reservations cannot be atomic.");
|
|
1370
|
+
this.#mode = this.#atomic ? "distributed" : "observational";
|
|
1371
|
+
if (this.#atomic) {
|
|
1372
|
+
this.reserve = (key, opts) => this.#reserve(key, opts);
|
|
1373
|
+
this.reconcile = (key, update) => this.#reconcile(key, update);
|
|
1374
|
+
this.extendGlobalReset = (scope, waitMs) => this.#extendGlobal(scope, waitMs);
|
|
1375
|
+
this.release = (key, grant, refund) => this.#release(key, grant, refund);
|
|
1376
|
+
this.reservesGlobal = true;
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
get lastError() {
|
|
1380
|
+
return this.#lastError;
|
|
1381
|
+
}
|
|
1382
|
+
isHealthy() {
|
|
1383
|
+
return this.#lastError === undefined;
|
|
1384
|
+
}
|
|
1385
|
+
get mode() {
|
|
1386
|
+
return this.#mode;
|
|
1387
|
+
}
|
|
1388
|
+
get supportsReservation() {
|
|
1389
|
+
return this.#atomic;
|
|
1390
|
+
}
|
|
1391
|
+
diagnostics() {
|
|
1392
|
+
const error = this.#lastError;
|
|
1393
|
+
return {
|
|
1394
|
+
mode: this.#mode,
|
|
1395
|
+
failurePolicy: this.#policy,
|
|
1396
|
+
atomic: this.#atomic,
|
|
1397
|
+
failures: this.#failures,
|
|
1398
|
+
lastFailureAt: this.#lastFailureAt,
|
|
1399
|
+
lastError: error === undefined ? undefined : String(sanitizeCause(error)?.message ?? error)
|
|
1400
|
+
};
|
|
1401
|
+
}
|
|
1402
|
+
async verify() {
|
|
1403
|
+
if (!this.#atomic)
|
|
1404
|
+
throw new TypeError("The Redis client has no eval method: reservations cannot be atomic.");
|
|
1405
|
+
try {
|
|
1406
|
+
await this.#withTimeout(this.#client.eval(VERIFY_SCRIPT, 0));
|
|
1407
|
+
} catch (error) {
|
|
1408
|
+
throw new RESTError2("The Redis rate-limit store cannot run its scripts.", 0, undefined, undefined, { kind: "coordination", cause: sanitizeCause(error) });
|
|
1409
|
+
}
|
|
1410
|
+
}
|
|
1411
|
+
#setMode(mode) {
|
|
1412
|
+
if (mode === this.#mode)
|
|
1413
|
+
return;
|
|
1414
|
+
const previous = this.#mode;
|
|
1415
|
+
this.#mode = mode;
|
|
1416
|
+
emitTelemetry(this.#telemetry, { type: "mode", mode, previous });
|
|
1417
|
+
console.warn(mode === "degraded-local" ? "[lunibee/rest] Redis rate-limit store unreachable: limiting locally in this process until it answers." : mode === "unavailable" ? "[lunibee/rest] Redis rate-limit store unreachable: requests fail until it answers (fail-closed)." : "[lunibee/rest] Redis rate-limit store reachable again: limits are shared.");
|
|
1418
|
+
}
|
|
1419
|
+
#withTimeout(promise) {
|
|
1420
|
+
let timer;
|
|
1421
|
+
return Promise.race([
|
|
1422
|
+
promise,
|
|
1423
|
+
new Promise((_, reject) => {
|
|
1424
|
+
timer = setTimeout(() => reject(new Error(`Redis command timed out after ${this.#timeout}ms`)), this.#timeout);
|
|
1425
|
+
})
|
|
1426
|
+
]).finally(() => clearTimeout(timer));
|
|
1427
|
+
}
|
|
1428
|
+
async#run(operation, command, fallback, enforcing) {
|
|
1429
|
+
const started = performance.now();
|
|
1430
|
+
try {
|
|
1431
|
+
const value = await this.#withTimeout(command());
|
|
1432
|
+
emitTelemetry(this.#telemetry, {
|
|
1433
|
+
type: "store",
|
|
1434
|
+
operation,
|
|
1435
|
+
latencyMs: performance.now() - started,
|
|
1436
|
+
ok: true
|
|
1437
|
+
});
|
|
1438
|
+
this.#lastError = undefined;
|
|
1439
|
+
this.#setMode(this.#atomic ? "distributed" : "observational");
|
|
1440
|
+
return value;
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
emitTelemetry(this.#telemetry, {
|
|
1443
|
+
type: "store",
|
|
1444
|
+
operation,
|
|
1445
|
+
latencyMs: performance.now() - started,
|
|
1446
|
+
ok: false
|
|
1447
|
+
});
|
|
1448
|
+
this.#lastError = error;
|
|
1449
|
+
this.#lastFailureAt = Date.now();
|
|
1450
|
+
this.#failures++;
|
|
1451
|
+
if (!this.#warnedSlots && /CROSSSLOT/.test(String(error))) {
|
|
1452
|
+
this.#warnedSlots = true;
|
|
1453
|
+
console.warn('[lunibee/rest] Redis Cluster refused a script whose keys hash to different slots (CROSSSLOT): use a hash-tagged prefix such as "{mybot}:rest:".');
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
this.#onErrorCallback?.(operation, error);
|
|
1457
|
+
} catch {}
|
|
1458
|
+
if (this.#policy === "fail-closed") {
|
|
1459
|
+
this.#setMode("unavailable");
|
|
1460
|
+
if (enforcing)
|
|
1461
|
+
throw new RESTError2(`The Redis rate-limit store is unavailable (${operation}); request not sent (failurePolicy: "fail-closed").`, 0, undefined, undefined, { kind: "coordination", cause: sanitizeCause(error) });
|
|
1462
|
+
} else
|
|
1463
|
+
this.#setMode("degraded-local");
|
|
1464
|
+
return fallback();
|
|
1465
|
+
}
|
|
1466
|
+
}
|
|
1467
|
+
#bucketKey(key) {
|
|
1468
|
+
return `${this.#prefix}bucket:${key}`;
|
|
1469
|
+
}
|
|
1470
|
+
#globalKey(scope) {
|
|
1471
|
+
return `${this.#prefix}global:${scope}`;
|
|
1472
|
+
}
|
|
1473
|
+
async getBucketHash(route) {
|
|
1474
|
+
return this.#run("getBucketHash", async () => await this.#client.get(`${this.#prefix}route:${route}`) ?? undefined, () => this.#local.getBucketHash(route), false);
|
|
1475
|
+
}
|
|
1476
|
+
async setBucketHash(route, hash) {
|
|
1477
|
+
this.#local.setBucketHash(route, hash);
|
|
1478
|
+
await this.#run("setBucketHash", async () => {
|
|
1479
|
+
await this.#client.set(`${this.#prefix}route:${route}`, hash, "EX", 604800);
|
|
1480
|
+
}, () => {
|
|
1481
|
+
return;
|
|
1482
|
+
}, false);
|
|
1483
|
+
}
|
|
1484
|
+
async getBucket(key) {
|
|
1485
|
+
return this.#run("getBucket", async () => {
|
|
1486
|
+
const data = await this.#client.get(this.#bucketKey(key));
|
|
1487
|
+
if (!data)
|
|
1488
|
+
return;
|
|
1489
|
+
try {
|
|
1490
|
+
return JSON.parse(data);
|
|
1491
|
+
} catch {
|
|
1492
|
+
return;
|
|
1493
|
+
}
|
|
1494
|
+
}, () => this.#local.getBucket(key), true);
|
|
1495
|
+
}
|
|
1496
|
+
async updateBucket(key, state) {
|
|
1497
|
+
this.#local.updateBucket(key, state);
|
|
1498
|
+
const ttl = Math.ceil(state.resetAt - Date.now());
|
|
1499
|
+
await this.#run("updateBucket", async () => {
|
|
1500
|
+
if (ttl <= 0)
|
|
1501
|
+
await this.#client.del(this.#bucketKey(key));
|
|
1502
|
+
else
|
|
1503
|
+
await this.#client.set(this.#bucketKey(key), JSON.stringify(state), "PX", ttl);
|
|
1504
|
+
}, () => {
|
|
1505
|
+
return;
|
|
1506
|
+
}, false);
|
|
1507
|
+
}
|
|
1508
|
+
reserve;
|
|
1509
|
+
reconcile;
|
|
1510
|
+
extendGlobalReset;
|
|
1511
|
+
release;
|
|
1512
|
+
reservesGlobal;
|
|
1513
|
+
async#reserve(key, options = {}) {
|
|
1514
|
+
const keys = [this.#bucketKey(key)];
|
|
1515
|
+
if (options.global !== undefined) {
|
|
1516
|
+
keys.push(this.#globalKey(options.global));
|
|
1517
|
+
if (options.globalLimit)
|
|
1518
|
+
keys.push(`${this.#prefix}global-log:${options.global}`);
|
|
1519
|
+
}
|
|
1520
|
+
return this.#run("reserve", async () => {
|
|
1521
|
+
const result = await this.#client.eval(RESERVE_SCRIPT, keys.length, ...keys, options.probeMs ?? DEFAULT_PROBE_MS, PROBE_POLL_MS, BUCKET_LINGER_MS, options.globalLimit ?? 0, GLOBAL_WINDOW_MS);
|
|
1522
|
+
const reservation = parseReservation(result);
|
|
1523
|
+
if (reservation.granted && reservation.known && this.#local.getBucket(key))
|
|
1524
|
+
this.#local.reserve(key, { probeMs: options.probeMs });
|
|
1525
|
+
return reservation;
|
|
1526
|
+
}, () => this.#local.reserve(key, options), true);
|
|
1527
|
+
}
|
|
1528
|
+
async#reconcile(key, update) {
|
|
1529
|
+
this.#local.reconcile(key, update);
|
|
1530
|
+
const arg = (value) => value === undefined ? "" : String(value);
|
|
1531
|
+
await this.#run("reconcile", async () => {
|
|
1532
|
+
await this.#client.eval(RECONCILE_SCRIPT, 1, this.#bucketKey(key), arg(update.remaining), arg(update.resetAfterMs), arg(update.limit), arg(update.window), arg(update.retryAfterMs), BUCKET_LINGER_MS);
|
|
1533
|
+
}, () => {
|
|
1534
|
+
return;
|
|
1535
|
+
}, false);
|
|
1536
|
+
}
|
|
1537
|
+
async#extendGlobal(scope, waitMs) {
|
|
1538
|
+
this.#local.extendGlobalReset(scope, waitMs);
|
|
1539
|
+
await this.#run("extendGlobalReset", async () => {
|
|
1540
|
+
await this.#client.eval(EXTEND_GLOBAL_SCRIPT, 1, this.#globalKey(scope), Math.max(0, Math.ceil(waitMs)));
|
|
1541
|
+
}, () => {
|
|
1542
|
+
return;
|
|
1543
|
+
}, false);
|
|
1544
|
+
}
|
|
1545
|
+
async#release(key, grant, refund) {
|
|
1546
|
+
this.#local.release(key, grant, refund);
|
|
1547
|
+
if (grant.resetAt === undefined)
|
|
1548
|
+
return;
|
|
1549
|
+
await this.#run("release", async () => {
|
|
1550
|
+
await this.#client.eval(RELEASE_SCRIPT, 1, this.#bucketKey(key), grant.resetAt, refund ? "1" : "0");
|
|
1551
|
+
}, () => {
|
|
1552
|
+
return;
|
|
1553
|
+
}, false);
|
|
1554
|
+
}
|
|
1555
|
+
async getGlobalReset(scope = "") {
|
|
1556
|
+
return this.#run("getGlobalReset", async () => {
|
|
1557
|
+
const resetAt = await this.#client.get(this.#globalKey(scope));
|
|
1558
|
+
return resetAt ? Number(resetAt) || 0 : 0;
|
|
1559
|
+
}, () => this.#local.getGlobalReset(scope), true);
|
|
1560
|
+
}
|
|
1561
|
+
async setGlobalReset(resetAt, scope = "") {
|
|
1562
|
+
this.#local.setGlobalReset(resetAt, scope);
|
|
1563
|
+
const ttl = Math.ceil(resetAt - Date.now());
|
|
1564
|
+
await this.#run("setGlobalReset", async () => {
|
|
1565
|
+
if (ttl <= 0)
|
|
1566
|
+
await this.#client.del(this.#globalKey(scope));
|
|
1567
|
+
else
|
|
1568
|
+
await this.#client.set(this.#globalKey(scope), String(resetAt), "PX", ttl);
|
|
1569
|
+
}, () => {
|
|
1570
|
+
return;
|
|
1571
|
+
}, false);
|
|
1572
|
+
}
|
|
1573
|
+
}
|
|
1574
|
+
function parseReservation(result) {
|
|
1575
|
+
if (!Array.isArray(result) || result.length < 3)
|
|
1576
|
+
throw new Error("Unexpected reply from the reservation script.");
|
|
1577
|
+
const [granted, value, flag] = result;
|
|
1578
|
+
const n = Number(value);
|
|
1579
|
+
if (!Number.isFinite(n))
|
|
1580
|
+
throw new Error("Unexpected reply from the reservation script.");
|
|
1581
|
+
if (Number(granted) === 1)
|
|
1582
|
+
return { granted: true, known: Number(flag) === 1, resetAt: n };
|
|
1583
|
+
const reason = String(flag);
|
|
1584
|
+
return {
|
|
1585
|
+
granted: false,
|
|
1586
|
+
retryAfterMs: Math.max(0, n),
|
|
1587
|
+
reason: reason === "global" || reason === "probe" ? reason : "bucket"
|
|
1588
|
+
};
|
|
1589
|
+
}
|
|
1590
|
+
// packages/rest/src/webhook.ts
|
|
1591
|
+
class WebhookClient2 {
|
|
1592
|
+
id;
|
|
1593
|
+
token;
|
|
1594
|
+
#rest;
|
|
1595
|
+
constructor(options, restOptions) {
|
|
1596
|
+
if (options.url) {
|
|
1597
|
+
const match = options.url.match(/^https:\/\/(?:(?:ptb|canary)\.)?discord(?:app)?\.com\/api(?:\/v\d+)?\/webhooks\/(\d{1,20})\/([A-Za-z0-9_-]+)\/?(?:[?#].*)?$/);
|
|
1598
|
+
if (!match)
|
|
1599
|
+
throw new Error("Invalid Discord webhook URL format.");
|
|
1600
|
+
this.id = match[1];
|
|
1601
|
+
this.token = match[2];
|
|
1602
|
+
} else if (options.id && options.token) {
|
|
1603
|
+
this.id = options.id;
|
|
1604
|
+
this.token = options.token;
|
|
1605
|
+
} else {
|
|
1606
|
+
throw new Error("WebhookClient requires either a webhook 'url' or both 'id' and 'token'.");
|
|
1607
|
+
}
|
|
1608
|
+
this.#rest = new REST2(restOptions);
|
|
1609
|
+
}
|
|
1610
|
+
#prepare(options, base = {}) {
|
|
1611
|
+
const payload = typeof options === "string" ? { content: options } : options;
|
|
1612
|
+
const { thread_id, files, ...body } = payload;
|
|
1613
|
+
const params = new URLSearchParams(base);
|
|
1614
|
+
if (thread_id)
|
|
1615
|
+
params.set("thread_id", String(thread_id));
|
|
1616
|
+
const json = { ...body };
|
|
1617
|
+
if (body.embeds)
|
|
1618
|
+
json.embeds = body.embeds.map((e) => ("toJSON" in e) ? e.toJSON() : e);
|
|
1619
|
+
if (body.components)
|
|
1620
|
+
json.components = body.components.map((c) => ("toJSON" in c) ? c.toJSON() : c);
|
|
1621
|
+
const text = params.toString();
|
|
1622
|
+
return { query: text ? `?${text}` : "", json, files };
|
|
1623
|
+
}
|
|
1624
|
+
async send(options) {
|
|
1625
|
+
const { query, json, files } = this.#prepare(options, { wait: "true" });
|
|
1626
|
+
const path = `${Routes2.webhook(this.id, this.token)}${query}`;
|
|
1627
|
+
return files?.length ? this.#rest.postWithFiles(path, json, files) : this.#rest.post(path, json);
|
|
1628
|
+
}
|
|
1629
|
+
async fetchMessage(messageId, options = {}) {
|
|
1630
|
+
const query = options.threadId ? `?${new URLSearchParams({ thread_id: options.threadId })}` : "";
|
|
1631
|
+
return this.#rest.get(`${Routes2.webhookMessage(this.id, this.token, messageId)}${query}`);
|
|
1632
|
+
}
|
|
1633
|
+
async editMessage(messageId, options) {
|
|
1634
|
+
const { query, json, files } = this.#prepare(options);
|
|
1635
|
+
const path = `${Routes2.webhookMessage(this.id, this.token, messageId)}${query}`;
|
|
1636
|
+
return files?.length ? this.#rest.patchWithFiles(path, json, files) : this.#rest.patch(path, json);
|
|
1637
|
+
}
|
|
1638
|
+
async deleteMessage(messageId, options = {}) {
|
|
1639
|
+
const query = options.threadId ? `?${new URLSearchParams({ thread_id: options.threadId })}` : "";
|
|
1640
|
+
await this.#rest.delete(`${Routes2.webhookMessage(this.id, this.token, messageId)}${query}`);
|
|
1641
|
+
}
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
// packages/rest/src/index.ts
|
|
1645
|
+
var LIBRARY_VERSION = package_default.version;
|
|
1646
|
+
var LIBRARY_URL = "https://github.com/Ekretos/lunibee";
|
|
1647
|
+
var USER_AGENT = `DiscordBot (${LIBRARY_URL}, ${LIBRARY_VERSION})`;
|
|
1648
|
+
function assertSafePath(path) {
|
|
1649
|
+
if (/[\u0000-\u0020\u007f#]/.test(path))
|
|
1650
|
+
throw new TypeError("REST paths must not contain control characters, spaces or '#'.");
|
|
1651
|
+
const end = path.indexOf("?");
|
|
1652
|
+
const pathname = end === -1 ? path : path.slice(0, end);
|
|
1653
|
+
if (pathname.includes("\\"))
|
|
1654
|
+
throw new TypeError("REST paths must not contain backslashes.");
|
|
1655
|
+
if (pathname.includes("//"))
|
|
1656
|
+
throw new TypeError("REST paths must not contain empty segments ('//').");
|
|
1657
|
+
for (const segment of pathname.split("/")) {
|
|
1658
|
+
let decoded;
|
|
1659
|
+
try {
|
|
1660
|
+
decoded = decodeURIComponent(segment);
|
|
1661
|
+
} catch {
|
|
1662
|
+
throw new TypeError("REST path has malformed percent-encoding.");
|
|
1663
|
+
}
|
|
1664
|
+
const bare = decoded.replace(/[\u0000-\u0020\u007f]/g, "");
|
|
1665
|
+
if (bare === "." || bare === ".." || /[\\/]/.test(decoded) && decoded.split(/[\\/]/).some((s) => {
|
|
1666
|
+
const part = s.replace(/[\u0000-\u0020\u007f]/g, "");
|
|
1667
|
+
return part === "." || part === "..";
|
|
1668
|
+
}))
|
|
1669
|
+
throw new TypeError("REST paths must not contain '.' or '..' segments.");
|
|
1670
|
+
}
|
|
1671
|
+
}
|
|
1672
|
+
var WRAPPER_KEYS = new Set([
|
|
1673
|
+
"body",
|
|
1674
|
+
"files",
|
|
1675
|
+
"query",
|
|
1676
|
+
"headers",
|
|
1677
|
+
"reason",
|
|
1678
|
+
"signal",
|
|
1679
|
+
"auth",
|
|
1680
|
+
"timeout"
|
|
1681
|
+
]);
|
|
1682
|
+
function wrapperError(verb, body) {
|
|
1683
|
+
if (typeof body !== "object" || body === null || Array.isArray(body))
|
|
1684
|
+
return;
|
|
1685
|
+
const keys = Object.keys(body);
|
|
1686
|
+
if ((keys.includes("body") || keys.includes("files")) && keys.every((key) => WRAPPER_KEYS.has(key)))
|
|
1687
|
+
return new TypeError(`rest.${verb}(path, { body, \u2026 }) was removed in 0.4.0: call rest.${verb}(path, body, options). Run \`lunibee migrate\` to find the calls.`);
|
|
1688
|
+
return;
|
|
1689
|
+
}
|
|
1690
|
+
function createRetryPolicy2(maxRetries = 2) {
|
|
1691
|
+
if (!Number.isFinite(maxRetries) || maxRetries < 0)
|
|
1692
|
+
throw new TypeError("maxRetries must be a non-negative finite number.");
|
|
1693
|
+
return {
|
|
1694
|
+
maxRetries: Math.floor(maxRetries),
|
|
1695
|
+
shouldRetry(method, status) {
|
|
1696
|
+
const normalized = method.toUpperCase();
|
|
1697
|
+
const idempotent = ["GET", "HEAD", "PUT", "DELETE"].includes(normalized);
|
|
1698
|
+
if (status === 0)
|
|
1699
|
+
return idempotent;
|
|
1700
|
+
return status === 429 || status >= 500 && status <= 599 && idempotent;
|
|
1701
|
+
},
|
|
1702
|
+
getDelay(attempt, retryAfter) {
|
|
1703
|
+
if (retryAfter !== undefined)
|
|
1704
|
+
return Math.max(0, retryAfter * 1000);
|
|
1705
|
+
return Math.min(1e4, 500 * 2 ** attempt) + Math.random() * 250;
|
|
1706
|
+
}
|
|
1707
|
+
};
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
class QueueWaitExceeded extends Error {
|
|
1711
|
+
ms;
|
|
1712
|
+
constructor(ms) {
|
|
1713
|
+
super("maxQueueWait exceeded");
|
|
1714
|
+
this.ms = ms;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
function checkWait(ms) {
|
|
1718
|
+
if (ms === undefined)
|
|
1719
|
+
return;
|
|
1720
|
+
if (!(ms >= 0) || !Number.isFinite(ms))
|
|
1721
|
+
throw new RangeError("maxQueueWait must be a non-negative number.");
|
|
1722
|
+
return ms;
|
|
1723
|
+
}
|
|
1724
|
+
|
|
1725
|
+
class REST2 {
|
|
1726
|
+
#retryPolicy;
|
|
1727
|
+
#token;
|
|
1728
|
+
#fingerprint;
|
|
1729
|
+
#telemetry;
|
|
1730
|
+
#closing = new AbortController;
|
|
1731
|
+
#inFlight = 0;
|
|
1732
|
+
#idle;
|
|
1733
|
+
#rejected;
|
|
1734
|
+
#stopOnInvalidToken;
|
|
1735
|
+
#maxQueueWait;
|
|
1736
|
+
#invalid = new Array(10).fill(0);
|
|
1737
|
+
#invalidMinute = 0;
|
|
1738
|
+
#drained;
|
|
1739
|
+
#limiter;
|
|
1740
|
+
#scheduler;
|
|
1741
|
+
#transport;
|
|
1742
|
+
#decoder = new ResponseDecoder2;
|
|
1743
|
+
#hooks = {};
|
|
1744
|
+
#concurrentBuckets;
|
|
1745
|
+
constructor(options = {}) {
|
|
1746
|
+
this.#token = options.token;
|
|
1747
|
+
this.#fingerprint = options.token ? tokenFingerprint2(options.token) : undefined;
|
|
1748
|
+
this.#telemetry = options.telemetry;
|
|
1749
|
+
this.#scheduler = new RequestScheduler2({
|
|
1750
|
+
maxQueued: options.maxQueued,
|
|
1751
|
+
maxQueuedPerBucket: options.maxQueuedPerBucket
|
|
1752
|
+
});
|
|
1753
|
+
this.#retryPolicy = options.retryPolicy ?? createRetryPolicy2(options.retries ?? 2);
|
|
1754
|
+
this.#transport = options.transport ?? new HttpTransport2({
|
|
1755
|
+
baseURL: options.baseURL,
|
|
1756
|
+
allowInsecureBaseURL: options.allowInsecureBaseURL,
|
|
1757
|
+
timeout: options.timeout
|
|
1758
|
+
});
|
|
1759
|
+
const store = options.store ?? new MemoryRateLimitStore2;
|
|
1760
|
+
this.#concurrentBuckets = (options.concurrentBuckets ?? true) && typeof store.reserve === "function";
|
|
1761
|
+
this.#limiter = new RateLimiter2(store, {
|
|
1762
|
+
concurrent: this.#concurrentBuckets,
|
|
1763
|
+
maxBackoff: options.maxBackoff,
|
|
1764
|
+
probeMs: this.#transport.timeout > 0 ? this.#transport.timeout + 1000 : undefined,
|
|
1765
|
+
telemetry: options.telemetry,
|
|
1766
|
+
globalLimit: options.globalLimit === false ? undefined : options.globalLimit ?? 50
|
|
1767
|
+
});
|
|
1768
|
+
this.#stopOnInvalidToken = options.stopOnInvalidToken ?? true;
|
|
1769
|
+
this.#maxQueueWait = checkWait(options.maxQueueWait);
|
|
1770
|
+
if (options.hooks)
|
|
1771
|
+
this.#hooks = options.hooks;
|
|
1772
|
+
}
|
|
1773
|
+
get store() {
|
|
1774
|
+
return this.#limiter.store;
|
|
1775
|
+
}
|
|
1776
|
+
setHooks(hooks) {
|
|
1777
|
+
this.#hooks = hooks;
|
|
1778
|
+
return this;
|
|
1779
|
+
}
|
|
1780
|
+
setToken(token) {
|
|
1781
|
+
if (!token.trim())
|
|
1782
|
+
throw new TypeError("A Discord bot token is required.");
|
|
1783
|
+
this.#token = token;
|
|
1784
|
+
this.#fingerprint = tokenFingerprint2(token);
|
|
1785
|
+
this.#rejected = undefined;
|
|
1786
|
+
}
|
|
1787
|
+
diagnostics() {
|
|
1788
|
+
const store = this.#limiter.store;
|
|
1789
|
+
return {
|
|
1790
|
+
mode: store.mode ?? (store instanceof MemoryRateLimitStore2 ? "local" : "distributed"),
|
|
1791
|
+
queued: this.#scheduler.queued,
|
|
1792
|
+
inFlight: this.#inFlight,
|
|
1793
|
+
buckets: this.#scheduler.size,
|
|
1794
|
+
closed: this.#closing.signal.aborted,
|
|
1795
|
+
invalidRequests: this.#invalidCount(0),
|
|
1796
|
+
tokenRejected: this.#rejected !== undefined && this.#rejected === this.#fingerprint
|
|
1797
|
+
};
|
|
1798
|
+
}
|
|
1799
|
+
async close() {
|
|
1800
|
+
if (!this.#closing.signal.aborted)
|
|
1801
|
+
this.#closing.abort(new Error("The REST client was closed."));
|
|
1802
|
+
if (this.#inFlight === 0)
|
|
1803
|
+
return;
|
|
1804
|
+
this.#drained ??= new Promise((resolve) => {
|
|
1805
|
+
this.#idle = resolve;
|
|
1806
|
+
});
|
|
1807
|
+
await this.#drained;
|
|
1808
|
+
}
|
|
1809
|
+
#invalidCount(add) {
|
|
1810
|
+
const minute = Math.floor(Date.now() / 60000);
|
|
1811
|
+
const elapsed = Math.min(10, minute - this.#invalidMinute);
|
|
1812
|
+
for (let i = 1;i <= elapsed; i++)
|
|
1813
|
+
this.#invalid[(this.#invalidMinute + i) % 10] = 0;
|
|
1814
|
+
if (elapsed > 0)
|
|
1815
|
+
this.#invalidMinute = minute;
|
|
1816
|
+
this.#invalid[minute % 10] += add;
|
|
1817
|
+
return this.#invalid.reduce((a, b) => a + b, 0);
|
|
1818
|
+
}
|
|
1819
|
+
#queueChanged() {
|
|
1820
|
+
emitTelemetry(this.#telemetry, {
|
|
1821
|
+
type: "queue",
|
|
1822
|
+
queued: this.#scheduler.queued,
|
|
1823
|
+
inFlight: this.#inFlight
|
|
1824
|
+
});
|
|
1825
|
+
}
|
|
1826
|
+
async request(method, path, body, options = {}) {
|
|
1827
|
+
if (typeof method !== "string" || !method.toUpperCase())
|
|
1828
|
+
throw new TypeError("REST method is required.");
|
|
1829
|
+
if (typeof path !== "string" || !path.startsWith("/"))
|
|
1830
|
+
throw new TypeError("REST paths must start with '/'.");
|
|
1831
|
+
assertSafePath(path);
|
|
1832
|
+
const route = createRouteKey2(method, path);
|
|
1833
|
+
const files = options.files;
|
|
1834
|
+
if (files && files.length > 0 && !(body instanceof FormData))
|
|
1835
|
+
body = this.#fileForm(body, await Promise.all(files.map(async (file) => ({
|
|
1836
|
+
...file,
|
|
1837
|
+
data: await file.data
|
|
1838
|
+
}))));
|
|
1839
|
+
const query = serializeQuery2(options.query);
|
|
1840
|
+
const requestPath = query ? `${path}${query}` : path;
|
|
1841
|
+
if (this.#closing.signal.aborted)
|
|
1842
|
+
throw abortError2(path, this.#closing.signal.reason);
|
|
1843
|
+
const identity = requestIdentity2(path, options.auth !== false && this.#token ? this.#fingerprint : undefined);
|
|
1844
|
+
if (identity.bucket === this.#fingerprint && this.#rejected === this.#fingerprint)
|
|
1845
|
+
throw new RESTError2("Discord rejected this bot token (401); request not sent. Call setToken() with a valid token.", 401, undefined, undefined, { method: route.method, path, kind: "authentication" });
|
|
1846
|
+
const queueWait = checkWait(options.maxQueueWait) ?? this.#maxQueueWait;
|
|
1847
|
+
let deadline;
|
|
1848
|
+
const sources = [this.#closing.signal];
|
|
1849
|
+
if (options.signal)
|
|
1850
|
+
sources.push(options.signal);
|
|
1851
|
+
if (queueWait !== undefined) {
|
|
1852
|
+
const expired = new AbortController;
|
|
1853
|
+
deadline = setTimeout(() => expired.abort(new QueueWaitExceeded(queueWait)), queueWait);
|
|
1854
|
+
sources.push(expired.signal);
|
|
1855
|
+
}
|
|
1856
|
+
const waitSignal = sources.length === 1 ? sources[0] : AbortSignal.any(sources);
|
|
1857
|
+
const state = { attempts: 0, status: 0 };
|
|
1858
|
+
const started = performance.now();
|
|
1859
|
+
this.#inFlight++;
|
|
1860
|
+
this.#queueChanged();
|
|
1861
|
+
try {
|
|
1862
|
+
return await this.#scheduler.runResolved(() => this.#limiter.resolveBucketKey(route.route, route.major, identity.bucket), waitSignal, path, (bucketKey, release) => {
|
|
1863
|
+
this.#queueChanged();
|
|
1864
|
+
return this.#attempts(route, requestPath, body, options, { identity, waitSignal, state }, bucketKey, release);
|
|
1865
|
+
});
|
|
1866
|
+
} catch (caught) {
|
|
1867
|
+
let error = caught;
|
|
1868
|
+
if (error instanceof RESTError2 && error.kind === "aborted" && error.cause instanceof QueueWaitExceeded)
|
|
1869
|
+
error = new RESTError2(`The request waited longer than maxQueueWait (${error.cause.ms}ms) before it could be sent.`, 0, undefined, undefined, { method: route.method, path, kind: "overloaded" });
|
|
1870
|
+
if (error instanceof RESTError2) {
|
|
1871
|
+
if (error.kind === "aborted" && state.attempts === 0)
|
|
1872
|
+
emitTelemetry(this.#telemetry, {
|
|
1873
|
+
type: "cancelled",
|
|
1874
|
+
stage: "queued"
|
|
1875
|
+
});
|
|
1876
|
+
else if (error.kind === "overloaded")
|
|
1877
|
+
emitTelemetry(this.#telemetry, { type: "overloaded" });
|
|
1878
|
+
if (state.status === 0)
|
|
1879
|
+
state.status = error.status;
|
|
1880
|
+
}
|
|
1881
|
+
throw error;
|
|
1882
|
+
} finally {
|
|
1883
|
+
clearTimeout(deadline);
|
|
1884
|
+
emitTelemetry(this.#telemetry, {
|
|
1885
|
+
type: "request",
|
|
1886
|
+
method: route.method,
|
|
1887
|
+
route: route.route,
|
|
1888
|
+
status: state.status,
|
|
1889
|
+
attempts: state.attempts,
|
|
1890
|
+
durationMs: performance.now() - started,
|
|
1891
|
+
ok: state.status >= 200 && state.status < 300
|
|
1892
|
+
});
|
|
1893
|
+
if (--this.#inFlight === 0 && this.#idle) {
|
|
1894
|
+
this.#idle();
|
|
1895
|
+
this.#idle = undefined;
|
|
1896
|
+
this.#drained = undefined;
|
|
1897
|
+
}
|
|
1898
|
+
this.#queueChanged();
|
|
1899
|
+
}
|
|
1900
|
+
}
|
|
1901
|
+
async#attempts(route, requestPath, body, options, context, initialBucketKey, release) {
|
|
1902
|
+
const { method, path } = route;
|
|
1903
|
+
const { identity, waitSignal, state } = context;
|
|
1904
|
+
const label = route.route;
|
|
1905
|
+
let bucketKey = initialBucketKey;
|
|
1906
|
+
for (let attempt = 0;attempt <= this.#retryPolicy.maxRetries; attempt++) {
|
|
1907
|
+
let grant;
|
|
1908
|
+
try {
|
|
1909
|
+
grant = await this.#limiter.acquire(bucketKey, waitSignal, path, {
|
|
1910
|
+
global: identity.global,
|
|
1911
|
+
resolve: () => this.#limiter.resolveBucketKey(route.route, route.major, identity.bucket)
|
|
1912
|
+
});
|
|
1913
|
+
if (grant.key)
|
|
1914
|
+
bucketKey = grant.key;
|
|
1915
|
+
} catch (error) {
|
|
1916
|
+
if (error instanceof RESTError2 && error.kind === "aborted")
|
|
1917
|
+
emitTelemetry(this.#telemetry, {
|
|
1918
|
+
type: "cancelled",
|
|
1919
|
+
stage: "waiting"
|
|
1920
|
+
});
|
|
1921
|
+
throw error;
|
|
1922
|
+
}
|
|
1923
|
+
if (this.#concurrentBuckets && grant.known)
|
|
1924
|
+
release();
|
|
1925
|
+
if (waitSignal.aborted) {
|
|
1926
|
+
await this.#limiter.releaseGrant(bucketKey, grant, true);
|
|
1927
|
+
emitTelemetry(this.#telemetry, {
|
|
1928
|
+
type: "cancelled",
|
|
1929
|
+
stage: "waiting"
|
|
1930
|
+
});
|
|
1931
|
+
throw abortError2(path, waitSignal.reason);
|
|
1932
|
+
}
|
|
1933
|
+
this.#emit(this.#hooks.onRequest, { method, path, attempt });
|
|
1934
|
+
state.attempts = attempt + 1;
|
|
1935
|
+
const requestStart = Date.now();
|
|
1936
|
+
let response;
|
|
1937
|
+
try {
|
|
1938
|
+
response = await this.#transport.send({
|
|
1939
|
+
method,
|
|
1940
|
+
path: requestPath,
|
|
1941
|
+
headers: this.#headers(body, options),
|
|
1942
|
+
body: this.#encodeBody(body),
|
|
1943
|
+
signal: options.signal
|
|
1944
|
+
});
|
|
1945
|
+
} catch (error) {
|
|
1946
|
+
const notSent = error instanceof RESTError2 && error.kind === "aborted";
|
|
1947
|
+
await this.#limiter.releaseGrant(bucketKey, grant, notSent);
|
|
1948
|
+
if (error instanceof RESTError2) {
|
|
1949
|
+
if (error.kind === "aborted")
|
|
1950
|
+
emitTelemetry(this.#telemetry, {
|
|
1951
|
+
type: "cancelled",
|
|
1952
|
+
stage: "transport"
|
|
1953
|
+
});
|
|
1954
|
+
throw error;
|
|
1955
|
+
}
|
|
1956
|
+
if (options.signal?.aborted) {
|
|
1957
|
+
emitTelemetry(this.#telemetry, {
|
|
1958
|
+
type: "cancelled",
|
|
1959
|
+
stage: "transport"
|
|
1960
|
+
});
|
|
1961
|
+
throw abortError2(path, options.signal.reason);
|
|
1962
|
+
}
|
|
1963
|
+
const failure = error instanceof TransportError2 ? error : new TransportError2("Discord REST request failed", false, sanitizeCause(error));
|
|
1964
|
+
emitTelemetry(this.#telemetry, {
|
|
1965
|
+
type: "networkError",
|
|
1966
|
+
timedOut: failure.timedOut
|
|
1967
|
+
});
|
|
1968
|
+
if (!failure.timedOut && attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, 0)) {
|
|
1969
|
+
const delay = this.#retryPolicy.getDelay(attempt);
|
|
1970
|
+
this.#emit(this.#hooks.onRetry, {
|
|
1971
|
+
method,
|
|
1972
|
+
path,
|
|
1973
|
+
attempt,
|
|
1974
|
+
status: 0,
|
|
1975
|
+
delayMs: delay
|
|
1976
|
+
});
|
|
1977
|
+
emitTelemetry(this.#telemetry, {
|
|
1978
|
+
type: "retry",
|
|
1979
|
+
route: label,
|
|
1980
|
+
reason: "network",
|
|
1981
|
+
attempt
|
|
1982
|
+
});
|
|
1983
|
+
await this.#retrySleep(delay, waitSignal, path);
|
|
1984
|
+
continue;
|
|
1985
|
+
}
|
|
1986
|
+
throw new RESTError2(failure.message, 0, undefined, undefined, {
|
|
1987
|
+
method,
|
|
1988
|
+
path,
|
|
1989
|
+
cause: failure.cause ?? failure,
|
|
1990
|
+
kind: failure.timedOut ? "timeout" : "network"
|
|
1991
|
+
});
|
|
1992
|
+
}
|
|
1993
|
+
state.status = response.status;
|
|
1994
|
+
if (response.status === 401 || response.status === 403 || response.status === 429 && response.headers.get("X-RateLimit-Scope") !== "shared")
|
|
1995
|
+
this.#invalidCount(1);
|
|
1996
|
+
if (response.status === 401 && this.#stopOnInvalidToken && identity.bucket === this.#fingerprint) {
|
|
1997
|
+
this.#rejected = this.#fingerprint;
|
|
1998
|
+
emitTelemetry(this.#telemetry, { type: "tokenRejected" });
|
|
1999
|
+
}
|
|
2000
|
+
const durationMs = Date.now() - requestStart;
|
|
2001
|
+
this.#emit(this.#hooks.onResponse, {
|
|
2002
|
+
method,
|
|
2003
|
+
path,
|
|
2004
|
+
status: response.status,
|
|
2005
|
+
durationMs
|
|
2006
|
+
});
|
|
2007
|
+
emitTelemetry(this.#telemetry, {
|
|
2008
|
+
type: "response",
|
|
2009
|
+
method,
|
|
2010
|
+
route: label,
|
|
2011
|
+
status: response.status,
|
|
2012
|
+
durationMs
|
|
2013
|
+
});
|
|
2014
|
+
const payload = await this.#decoder.read(response);
|
|
2015
|
+
const data = response.ok ? {} : this.#decoder.errorData(payload);
|
|
2016
|
+
bucketKey = await this.#limiter.applyResponse(response, bucketKey, route.route, route.major, {
|
|
2017
|
+
identity: identity.bucket,
|
|
2018
|
+
global: identity.global,
|
|
2019
|
+
body: data,
|
|
2020
|
+
grant
|
|
2021
|
+
});
|
|
2022
|
+
if (response.ok)
|
|
2023
|
+
return response.status === 204 ? undefined : payload;
|
|
2024
|
+
const limited = response.status === 429;
|
|
2025
|
+
const retryAfterMs = limited ? this.#limiter.retryAfterMs(response, data) : undefined;
|
|
2026
|
+
if (limited) {
|
|
2027
|
+
const global = data.global === true || response.headers.get("X-RateLimit-Global") === "true";
|
|
2028
|
+
const scope = response.headers.get("X-RateLimit-Scope")?.toLowerCase();
|
|
2029
|
+
emitTelemetry(this.#telemetry, {
|
|
2030
|
+
type: "rateLimited",
|
|
2031
|
+
route: label,
|
|
2032
|
+
scope: global ? "global" : scope === "user" || scope === "shared" ? scope : "unknown",
|
|
2033
|
+
retryAfterMs
|
|
2034
|
+
});
|
|
2035
|
+
this.#emit(this.#hooks.onRateLimit, {
|
|
2036
|
+
method,
|
|
2037
|
+
path,
|
|
2038
|
+
retryAfterMs,
|
|
2039
|
+
global,
|
|
2040
|
+
bucket: response.headers.get("X-RateLimit-Bucket") ?? await this.#limiter.store.getBucketHash(route.route)
|
|
2041
|
+
});
|
|
2042
|
+
}
|
|
2043
|
+
if (attempt < this.#retryPolicy.maxRetries && this.#retryPolicy.shouldRetry(method, response.status)) {
|
|
2044
|
+
const delay = this.#retryPolicy.getDelay(attempt, limited ? retryAfterMs / 1000 : undefined);
|
|
2045
|
+
if (delay > this.#limiter.maxBackoff)
|
|
2046
|
+
throw new RESTError2(`Discord asked to wait ${Math.ceil(delay / 1000)}s before retrying, longer than maxBackoff (${Math.ceil(this.#limiter.maxBackoff / 1000)}s).`, response.status, data.code, payload, { method, path });
|
|
2047
|
+
this.#emit(this.#hooks.onRetry, {
|
|
2048
|
+
method,
|
|
2049
|
+
path,
|
|
2050
|
+
attempt,
|
|
2051
|
+
status: response.status,
|
|
2052
|
+
delayMs: delay
|
|
2053
|
+
});
|
|
2054
|
+
emitTelemetry(this.#telemetry, {
|
|
2055
|
+
type: "retry",
|
|
2056
|
+
route: label,
|
|
2057
|
+
reason: limited ? "rateLimited" : "server",
|
|
2058
|
+
attempt
|
|
2059
|
+
});
|
|
2060
|
+
await this.#retrySleep(limited ? delay - retryAfterMs : delay, waitSignal, path);
|
|
2061
|
+
continue;
|
|
2062
|
+
}
|
|
2063
|
+
throw new RESTError2(data.message ?? response.statusText ?? `Discord REST request failed with status ${response.status}`, response.status, data.code, payload, { method, path });
|
|
2064
|
+
}
|
|
2065
|
+
throw new RESTError2("Discord REST request exhausted its retry attempts", 0, undefined, undefined, { method, path });
|
|
2066
|
+
}
|
|
2067
|
+
async#retrySleep(delay, signal, path) {
|
|
2068
|
+
try {
|
|
2069
|
+
await sleep(delay, signal, path);
|
|
2070
|
+
} catch (error) {
|
|
2071
|
+
emitTelemetry(this.#telemetry, {
|
|
2072
|
+
type: "cancelled",
|
|
2073
|
+
stage: "retry"
|
|
2074
|
+
});
|
|
2075
|
+
throw error;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
#headers(body, options) {
|
|
2079
|
+
const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
|
|
2080
|
+
const headers = { ...options.headers };
|
|
2081
|
+
const set = (name, value) => {
|
|
2082
|
+
for (const key of Object.keys(headers))
|
|
2083
|
+
if (key.toLowerCase() === name.toLowerCase())
|
|
2084
|
+
delete headers[key];
|
|
2085
|
+
headers[name] = value;
|
|
2086
|
+
};
|
|
2087
|
+
if (options.auth !== false && this.#token)
|
|
2088
|
+
set("Authorization", `Bot ${this.#token}`);
|
|
2089
|
+
set("User-Agent", USER_AGENT);
|
|
2090
|
+
if (options.reason !== undefined)
|
|
2091
|
+
set("X-Audit-Log-Reason", encodeURIComponent(options.reason));
|
|
2092
|
+
if (!isFormData)
|
|
2093
|
+
set("Content-Type", "application/json");
|
|
2094
|
+
return headers;
|
|
2095
|
+
}
|
|
2096
|
+
#encodeBody(body) {
|
|
2097
|
+
if (typeof FormData !== "undefined" && body instanceof FormData)
|
|
2098
|
+
return body;
|
|
2099
|
+
return body === undefined ? undefined : JSON.stringify(body);
|
|
2100
|
+
}
|
|
2101
|
+
get(path, options) {
|
|
2102
|
+
return this.request("GET", path, options?.body, options);
|
|
2103
|
+
}
|
|
2104
|
+
post(path, body, options) {
|
|
2105
|
+
const removed = wrapperError("post", body);
|
|
2106
|
+
if (removed)
|
|
2107
|
+
return Promise.reject(removed);
|
|
2108
|
+
return this.request("POST", path, body, options);
|
|
2109
|
+
}
|
|
2110
|
+
patch(path, body, options) {
|
|
2111
|
+
const removed = wrapperError("patch", body);
|
|
2112
|
+
if (removed)
|
|
2113
|
+
return Promise.reject(removed);
|
|
2114
|
+
return this.request("PATCH", path, body, options);
|
|
2115
|
+
}
|
|
2116
|
+
put(path, body, options) {
|
|
2117
|
+
const removed = wrapperError("put", body);
|
|
2118
|
+
if (removed)
|
|
2119
|
+
return Promise.reject(removed);
|
|
2120
|
+
return this.request("PUT", path, body, options);
|
|
2121
|
+
}
|
|
2122
|
+
delete(path, options) {
|
|
2123
|
+
return this.request("DELETE", path, options?.body, options);
|
|
2124
|
+
}
|
|
2125
|
+
#fileForm(payload, files) {
|
|
2126
|
+
const form = new FormData;
|
|
2127
|
+
form.append("payload_json", JSON.stringify(payload));
|
|
2128
|
+
for (let i = 0;i < files.length; i++) {
|
|
2129
|
+
const file = files[i];
|
|
2130
|
+
const blob = file.data instanceof Blob ? file.data : new Blob([file.data], {
|
|
2131
|
+
type: file.contentType ?? "application/octet-stream"
|
|
2132
|
+
});
|
|
2133
|
+
form.append(`files[${i}]`, blob, file.name.replace(/[\r\n"\\]/g, "_"));
|
|
2134
|
+
}
|
|
2135
|
+
return form;
|
|
2136
|
+
}
|
|
2137
|
+
postWithFiles(path, payload, files, options) {
|
|
2138
|
+
return this.request("POST", path, payload, { ...options, files });
|
|
2139
|
+
}
|
|
2140
|
+
patchWithFiles(path, payload, files, options) {
|
|
2141
|
+
return this.request("PATCH", path, payload, { ...options, files });
|
|
2142
|
+
}
|
|
2143
|
+
#emit(hook, ctx) {
|
|
2144
|
+
if (!hook)
|
|
2145
|
+
return;
|
|
2146
|
+
const withPath = ctx;
|
|
2147
|
+
if (typeof withPath.path === "string")
|
|
2148
|
+
ctx = { ...ctx, path: redactPath2(withPath.path) };
|
|
2149
|
+
Promise.resolve().then(() => hook(ctx)).catch(() => {});
|
|
2150
|
+
}
|
|
2151
|
+
}
|
|
2152
|
+
|
|
2153
|
+
export { MemoryRateLimitStore2, redactPath2, RESTError2, abortError2, normalizeRoutePath2, majorParameter2, scopeBucket2, createRouteKey2, serializeQuery2, RequestScheduler2, headerNumber2, parseRateLimitHeaders2, RESTMetrics2, DEFAULT_MAX_BACKOFF_MS2, RateLimiter2, tokenFingerprint2, requestIdentity2, TransportError2, HttpTransport2, ResponseDecoder2, Routes2, RESTErrorCode2, fromBunRedis2, RedisRateLimitStore2, WebhookClient2, createRetryPolicy2, REST2 };
|
|
2154
|
+
|
|
2155
|
+
//# debugId=D0A76CBA183E199B64756E2164756E21
|