snowtransfer 0.18.0 → 0.19.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/CHANGELOG.md +33 -0
- package/dist/Constants.d.ts +59 -0
- package/dist/Constants.js +123 -0
- package/dist/Endpoints.d.ts +120 -0
- package/dist/Endpoints.js +121 -0
- package/dist/RequestHandler.d.ts +258 -0
- package/dist/RequestHandler.js +629 -0
- package/dist/SnowTransfer.d.ts +70 -0
- package/dist/SnowTransfer.js +105 -0
- package/dist/StateMachine.d.ts +104 -0
- package/dist/StateMachine.js +238 -0
- package/dist/StateMachineGraph.d.ts +3 -0
- package/dist/StateMachineGraph.js +23 -0
- package/dist/Types.d.ts +76 -0
- package/dist/Types.js +2 -0
- package/dist/index.d.ts +25 -718
- package/dist/index.js +63 -44
- package/dist/methods/Assets.d.ts +290 -0
- package/dist/methods/Assets.js +326 -0
- package/dist/methods/AuditLog.d.ts +40 -0
- package/dist/methods/AuditLog.js +44 -0
- package/dist/methods/AutoModeration.d.ts +122 -0
- package/dist/methods/AutoModeration.js +135 -0
- package/dist/methods/Bot.d.ts +65 -0
- package/dist/methods/Bot.js +75 -0
- package/dist/methods/Channel.d.ts +866 -0
- package/dist/methods/Channel.js +982 -0
- package/dist/methods/Entitlements.d.ts +87 -0
- package/dist/methods/Entitlements.js +99 -0
- package/dist/methods/Guild.d.ts +722 -0
- package/dist/methods/Guild.js +785 -0
- package/dist/methods/GuildScheduledEvent.d.ts +138 -0
- package/dist/methods/GuildScheduledEvent.js +155 -0
- package/dist/methods/GuildTemplate.d.ts +110 -0
- package/dist/methods/GuildTemplate.js +124 -0
- package/dist/methods/Interaction.d.ts +339 -0
- package/dist/methods/Interaction.js +359 -0
- package/dist/methods/Invite.d.ts +81 -0
- package/dist/methods/Invite.js +107 -0
- package/dist/methods/Sku.d.ts +58 -0
- package/dist/methods/Sku.js +66 -0
- package/dist/methods/StageInstance.d.ts +86 -0
- package/dist/methods/StageInstance.js +97 -0
- package/dist/methods/User.d.ts +167 -0
- package/dist/methods/User.js +184 -0
- package/dist/methods/Voice.d.ts +44 -0
- package/dist/methods/Voice.js +52 -0
- package/dist/methods/Webhook.d.ts +265 -0
- package/dist/methods/Webhook.js +256 -0
- package/dist/tokenless.d.ts +19 -0
- package/dist/tokenless.js +31 -0
- package/package.json +9 -9
- package/dist/index.js.map +0 -1
|
@@ -0,0 +1,629 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/* eslint-disable no-async-promise-executor */
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.RequestHandler = exports.Ratelimiter = exports.Bucket = exports.LeakyCounter = exports.IntervalCounter = exports.DiscordAPIError = void 0;
|
|
5
|
+
const fs = require("node:fs");
|
|
6
|
+
const path = require("node:path");
|
|
7
|
+
const node_events_1 = require("node:events");
|
|
8
|
+
const util = require("node:util");
|
|
9
|
+
const nodeCrypto = require("node:crypto");
|
|
10
|
+
const Endpoints = require("./Endpoints");
|
|
11
|
+
const { version } = JSON.parse(fs.readFileSync(path.join(__dirname, "../package.json"), { encoding: "utf8" })); // otherwise, the json was included in the build
|
|
12
|
+
const Constants = require("./Constants");
|
|
13
|
+
const SM = require("./StateMachine");
|
|
14
|
+
// const applicationJSONRegex = /application\/json/;
|
|
15
|
+
const routeRegex = /\/([a-z-]+)\/(?:\d+)/g;
|
|
16
|
+
const reactionsRegex = /\/reactions\/[^/]+/g;
|
|
17
|
+
const reactionsUserRegex = /\/reactions\/:id\/[^/]+/g;
|
|
18
|
+
const webhooksRegex = /^\/webhooks\/(\d+)\/[A-Za-z0-9-_]{64,}/;
|
|
19
|
+
const isMessageEndpointRegex = /\/messages\/:id$/;
|
|
20
|
+
const isGuildChannelsRegex = /\/guilds\/\d+\/channels$/;
|
|
21
|
+
const messagesRegex = /\/messages\/\d+$/;
|
|
22
|
+
const disallowedBodyMethods = new Set(["head", "get", "delete"]);
|
|
23
|
+
/**
|
|
24
|
+
* @since 0.3.0
|
|
25
|
+
*/
|
|
26
|
+
class DiscordAPIError extends Error {
|
|
27
|
+
method;
|
|
28
|
+
path;
|
|
29
|
+
code;
|
|
30
|
+
httpStatus;
|
|
31
|
+
// @ts-expect-error Is assigned by Object.defineProperties
|
|
32
|
+
request;
|
|
33
|
+
// @ts-expect-error Is assigned by Object.defineProperties
|
|
34
|
+
response;
|
|
35
|
+
constructor(error, request, response) {
|
|
36
|
+
super();
|
|
37
|
+
this.name = "DiscordAPIError";
|
|
38
|
+
this.message = error.message ?? util.inspect(error);
|
|
39
|
+
this.method = request.method;
|
|
40
|
+
this.path = request.endpoint;
|
|
41
|
+
this.code = error.code ?? 4000;
|
|
42
|
+
this.httpStatus = response.status;
|
|
43
|
+
Object.defineProperties(this, {
|
|
44
|
+
request: { enumerable: false, value: request },
|
|
45
|
+
response: { enumerable: false, value: response },
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
exports.DiscordAPIError = DiscordAPIError;
|
|
50
|
+
/**
|
|
51
|
+
* @since 0.17.0
|
|
52
|
+
*/
|
|
53
|
+
class IntervalCounter {
|
|
54
|
+
limit;
|
|
55
|
+
reset;
|
|
56
|
+
/**
|
|
57
|
+
* Remaining amount of executions during the current timeframe
|
|
58
|
+
*/
|
|
59
|
+
remaining;
|
|
60
|
+
firstRequestTime = 0;
|
|
61
|
+
resetAt = null;
|
|
62
|
+
id = new Array(3).fill(0).map(() => String.fromCodePoint(Math.floor(Math.random() * 26 + 65))).join("");
|
|
63
|
+
/**
|
|
64
|
+
* Create a new base bucket
|
|
65
|
+
* @param limit Number of functions that may be executed during the timeframe set in reset
|
|
66
|
+
* @param reset Timeframe in milliseconds until the ratelimit resets after first
|
|
67
|
+
*/
|
|
68
|
+
constructor(limit, reset) {
|
|
69
|
+
this.limit = limit;
|
|
70
|
+
this.reset = reset;
|
|
71
|
+
this.remaining = limit;
|
|
72
|
+
}
|
|
73
|
+
checkReset() {
|
|
74
|
+
// Check if the counter has refreshed
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
// If specific resetAt, use it (ignoring preset reset interval)
|
|
77
|
+
if (this.resetAt !== null) {
|
|
78
|
+
if (now > this.resetAt) {
|
|
79
|
+
this.firstRequestTime = 0;
|
|
80
|
+
this.remaining = this.limit;
|
|
81
|
+
this.resetAt = null;
|
|
82
|
+
if (globalThis.snowtransferDebugLogging)
|
|
83
|
+
console.log(`${new Date().toISOString()} [itrv] [${this.id}] informed reset: ${this.remaining}/${this.limit}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// If no specific resetAt, count from first request and reset interval
|
|
87
|
+
else if (now > this.firstRequestTime + this.reset) {
|
|
88
|
+
this.firstRequestTime = 0;
|
|
89
|
+
this.remaining = this.limit;
|
|
90
|
+
if (globalThis.snowtransferDebugLogging)
|
|
91
|
+
console.log(`${new Date().toISOString()} [itrv] [${this.id}] predicted reset: ${this.remaining}/${this.limit}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
hasReset() {
|
|
95
|
+
this.checkReset();
|
|
96
|
+
return this.remaining === this.limit;
|
|
97
|
+
}
|
|
98
|
+
canTake() {
|
|
99
|
+
this.checkReset();
|
|
100
|
+
return this.remaining > 0;
|
|
101
|
+
}
|
|
102
|
+
take() {
|
|
103
|
+
this.checkReset();
|
|
104
|
+
if (this.remaining === this.limit)
|
|
105
|
+
this.firstRequestTime = Date.now();
|
|
106
|
+
if (this.remaining <= 0)
|
|
107
|
+
return false;
|
|
108
|
+
this.remaining--;
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
timeUntilReset() {
|
|
112
|
+
const now = Date.now();
|
|
113
|
+
if (this.resetAt)
|
|
114
|
+
return Math.max(this.resetAt - now + 1, 0);
|
|
115
|
+
else
|
|
116
|
+
return Math.max(this.firstRequestTime + this.reset - Date.now() + 1, 0);
|
|
117
|
+
}
|
|
118
|
+
// update the firstRequestTime to represent the *response* time to avoid getting fucked by slight network latency inconsistencies after a bucket refresh
|
|
119
|
+
responseReceived() {
|
|
120
|
+
if (this.remaining === this.limit - 1 && this.firstRequestTime)
|
|
121
|
+
this.firstRequestTime = Date.now();
|
|
122
|
+
}
|
|
123
|
+
applyCount(limit, remaining, resetAfter) {
|
|
124
|
+
if (limit != null)
|
|
125
|
+
this.limit = limit;
|
|
126
|
+
this.remaining = remaining;
|
|
127
|
+
this.resetAt = Date.now() + resetAfter;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
exports.IntervalCounter = IntervalCounter;
|
|
131
|
+
/**
|
|
132
|
+
* @since 0.17.0
|
|
133
|
+
*/
|
|
134
|
+
class LeakyCounter {
|
|
135
|
+
limit;
|
|
136
|
+
/**
|
|
137
|
+
* Remaining amount of executions during the current timeframe
|
|
138
|
+
*/
|
|
139
|
+
remaining;
|
|
140
|
+
resetAt = null;
|
|
141
|
+
id = new Array(3).fill(0).map(() => String.fromCodePoint(Math.floor(Math.random() * 26 + 65))).join("");
|
|
142
|
+
/**
|
|
143
|
+
* Create a new base bucket
|
|
144
|
+
* @param limit Number of functions that may be executed until some are reset
|
|
145
|
+
*/
|
|
146
|
+
constructor(limit) {
|
|
147
|
+
this.limit = limit;
|
|
148
|
+
this.remaining = limit;
|
|
149
|
+
}
|
|
150
|
+
checkReset() {
|
|
151
|
+
// Check if the counter has refreshed
|
|
152
|
+
const now = Date.now();
|
|
153
|
+
if (this.resetAt && now > this.resetAt) {
|
|
154
|
+
this.remaining = Math.min(this.limit, this.remaining + 1); // only restore one when it resets
|
|
155
|
+
this.resetAt = null;
|
|
156
|
+
if (globalThis.snowtransferDebugLogging)
|
|
157
|
+
console.log(`${new Date().toISOString()} [leak] [${this.id}] restore one: ${this.remaining}/${this.limit}`);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
hasReset() {
|
|
161
|
+
this.checkReset();
|
|
162
|
+
return this.remaining === this.limit;
|
|
163
|
+
}
|
|
164
|
+
canTake() {
|
|
165
|
+
this.checkReset();
|
|
166
|
+
return this.remaining > 0;
|
|
167
|
+
}
|
|
168
|
+
take() {
|
|
169
|
+
this.checkReset();
|
|
170
|
+
if (globalThis.snowtransferDebugLogging)
|
|
171
|
+
console.log(`${new Date().toISOString()} [leak] [${this.id}] took: ${this.remaining - 1} left`);
|
|
172
|
+
if (this.remaining <= 0)
|
|
173
|
+
return false;
|
|
174
|
+
this.remaining--;
|
|
175
|
+
return true;
|
|
176
|
+
}
|
|
177
|
+
timeUntilReset() {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
if (this.resetAt)
|
|
180
|
+
return Math.max(this.resetAt - now + 1, 0);
|
|
181
|
+
else
|
|
182
|
+
return 0;
|
|
183
|
+
}
|
|
184
|
+
responseReceived() { }
|
|
185
|
+
applyCount(limit, remaining, resetAfter) {
|
|
186
|
+
if (limit != null)
|
|
187
|
+
this.limit = limit;
|
|
188
|
+
this.remaining = remaining;
|
|
189
|
+
this.resetAt = Date.now() + resetAfter;
|
|
190
|
+
if (globalThis.snowtransferDebugLogging)
|
|
191
|
+
console.log(`${new Date().toISOString()} [leak] [${this.id}] applied: ${remaining}/${limit} - wait another ${resetAfter}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
exports.LeakyCounter = LeakyCounter;
|
|
195
|
+
/**
|
|
196
|
+
* Bucket used for saving ratelimits
|
|
197
|
+
* @since 0.1.0
|
|
198
|
+
*/
|
|
199
|
+
class Bucket {
|
|
200
|
+
/** Tracks the state this bucket is in (blocked, running, waiting, etc) and what operations are allowed. */
|
|
201
|
+
sm = new SM("ready");
|
|
202
|
+
/** Wrapped functions which always resolve (not reject) after the original function has completed and resolved. The original function may manipulate rate limit buckets in that time before it resolves. */
|
|
203
|
+
calls = [];
|
|
204
|
+
/** The backing counters of the bucket that determine how many functions can be consumed in a timeframe. */
|
|
205
|
+
counters = [];
|
|
206
|
+
pauseRequested = false;
|
|
207
|
+
/**
|
|
208
|
+
* Create a new bucket
|
|
209
|
+
*/
|
|
210
|
+
constructor(counters) {
|
|
211
|
+
this.counters = counters;
|
|
212
|
+
// nothing to do, nothing waiting for, the next request will be sent straight away
|
|
213
|
+
this.sm.defineState("ready", {
|
|
214
|
+
onEnter: [],
|
|
215
|
+
onLeave: [],
|
|
216
|
+
transitions: new Map([
|
|
217
|
+
["enqueue", {
|
|
218
|
+
destination: "check"
|
|
219
|
+
}],
|
|
220
|
+
["pause", {
|
|
221
|
+
destination: "paused"
|
|
222
|
+
}]
|
|
223
|
+
])
|
|
224
|
+
});
|
|
225
|
+
// trying to send a request, but should first check if there are any requests etc
|
|
226
|
+
this.sm.defineState("check", {
|
|
227
|
+
onEnter: [
|
|
228
|
+
() => {
|
|
229
|
+
if (this.pauseRequested) {
|
|
230
|
+
return this.sm.doTransition("pause");
|
|
231
|
+
}
|
|
232
|
+
if (!this.counters.every(c => c.canTake())) {
|
|
233
|
+
return this.sm.doTransition("cooldown");
|
|
234
|
+
}
|
|
235
|
+
if (this.calls.length) {
|
|
236
|
+
return this.sm.doTransition("run");
|
|
237
|
+
}
|
|
238
|
+
return this.sm.doTransition("empty");
|
|
239
|
+
}
|
|
240
|
+
],
|
|
241
|
+
onLeave: [],
|
|
242
|
+
transitions: new Map([
|
|
243
|
+
["pause", {
|
|
244
|
+
destination: "paused"
|
|
245
|
+
}],
|
|
246
|
+
["cooldown", {
|
|
247
|
+
destination: "cooldown"
|
|
248
|
+
}],
|
|
249
|
+
["run", {
|
|
250
|
+
destination: "running"
|
|
251
|
+
}],
|
|
252
|
+
["empty", {
|
|
253
|
+
destination: "ready"
|
|
254
|
+
}]
|
|
255
|
+
])
|
|
256
|
+
});
|
|
257
|
+
// a request is in progress, can't do anything else until it gets back (the bucket is sequential)
|
|
258
|
+
this.sm.defineState("running", {
|
|
259
|
+
onEnter: [
|
|
260
|
+
() => {
|
|
261
|
+
// Take one from each counter
|
|
262
|
+
this.counters.forEach(c => c.take());
|
|
263
|
+
// Dequeue and run
|
|
264
|
+
const cb = this.calls.shift();
|
|
265
|
+
cb().then(() => {
|
|
266
|
+
this.sm.doTransition("complete");
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
],
|
|
270
|
+
onLeave: [],
|
|
271
|
+
transitions: new Map([
|
|
272
|
+
["complete", {
|
|
273
|
+
destination: "check"
|
|
274
|
+
}]
|
|
275
|
+
])
|
|
276
|
+
});
|
|
277
|
+
// can't send more requests, waiting for rate limit reset. may or may not have pending requests
|
|
278
|
+
this.sm.defineState("cooldown", {
|
|
279
|
+
onEnter: [
|
|
280
|
+
() => {
|
|
281
|
+
const blocked = this.counters.filter(c => !c.canTake());
|
|
282
|
+
this.sm.doTransitionLater("reset", Math.max(...blocked.map(c => c.timeUntilReset()), 1));
|
|
283
|
+
}
|
|
284
|
+
],
|
|
285
|
+
onLeave: [],
|
|
286
|
+
transitions: new Map([
|
|
287
|
+
["reset", {
|
|
288
|
+
destination: "check"
|
|
289
|
+
}]
|
|
290
|
+
])
|
|
291
|
+
});
|
|
292
|
+
// user has requested nothing to be sent for now
|
|
293
|
+
this.sm.defineState("paused", {
|
|
294
|
+
onEnter: [],
|
|
295
|
+
onLeave: [
|
|
296
|
+
() => {
|
|
297
|
+
this.pauseRequested = false;
|
|
298
|
+
}
|
|
299
|
+
],
|
|
300
|
+
transitions: new Map([
|
|
301
|
+
["resume", {
|
|
302
|
+
destination: "check"
|
|
303
|
+
}]
|
|
304
|
+
])
|
|
305
|
+
});
|
|
306
|
+
this.sm.freeze();
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* Queue a function to be executed
|
|
310
|
+
* @since 0.12.0
|
|
311
|
+
* @param fn function to be executed
|
|
312
|
+
* @returns Result of the function if any
|
|
313
|
+
*/
|
|
314
|
+
enqueue(fn) {
|
|
315
|
+
return new Promise((resolve, reject) => {
|
|
316
|
+
this.calls.push(() => {
|
|
317
|
+
return fn(this).then(resolve).catch(reject);
|
|
318
|
+
});
|
|
319
|
+
if (this.sm.currentStateName === "ready")
|
|
320
|
+
this.sm.doTransition("enqueue");
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
/**
|
|
324
|
+
* Pause the bucket from consuming more functions until resumed
|
|
325
|
+
* @since 0.16.0
|
|
326
|
+
*/
|
|
327
|
+
pause() {
|
|
328
|
+
this.pauseRequested = true;
|
|
329
|
+
if (this.sm.currentStateName === "ready")
|
|
330
|
+
this.sm.doTransition("pause");
|
|
331
|
+
}
|
|
332
|
+
/**
|
|
333
|
+
* If the bucket is paused, resume it
|
|
334
|
+
* @since 0.16.0
|
|
335
|
+
*/
|
|
336
|
+
resume() {
|
|
337
|
+
this.pauseRequested = false;
|
|
338
|
+
if (this.sm.currentStateName !== "paused")
|
|
339
|
+
return;
|
|
340
|
+
this.sm.doTransition("resume");
|
|
341
|
+
}
|
|
342
|
+
/**
|
|
343
|
+
* Clear the current queue of events to be sent
|
|
344
|
+
* @since 0.1.0
|
|
345
|
+
*/
|
|
346
|
+
dropQueue() {
|
|
347
|
+
this.calls.length = 0;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
exports.Bucket = Bucket;
|
|
351
|
+
/**
|
|
352
|
+
* Ratelimiter used for handling the ratelimits imposed by the rest api
|
|
353
|
+
* @since 0.1.0
|
|
354
|
+
*/
|
|
355
|
+
class Ratelimiter {
|
|
356
|
+
/**
|
|
357
|
+
* A Map of Buckets keyed by route keys that store rate limit info
|
|
358
|
+
*/
|
|
359
|
+
buckets = new Map();
|
|
360
|
+
/**
|
|
361
|
+
* The bucket that limits how many requests per second you can make globally
|
|
362
|
+
*/
|
|
363
|
+
globalBucket = new Bucket([new IntervalCounter(Constants.GLOBAL_REQUESTS_PER_SECOND, 1000)]);
|
|
364
|
+
/**
|
|
365
|
+
* If you're being globally rate limited
|
|
366
|
+
*/
|
|
367
|
+
get global() {
|
|
368
|
+
return !this.globalBucket.counters[0].canTake();
|
|
369
|
+
}
|
|
370
|
+
constructor() {
|
|
371
|
+
setInterval(() => {
|
|
372
|
+
for (const [key, value] of this.buckets.entries()) {
|
|
373
|
+
const counter = value.counters[0];
|
|
374
|
+
if (value.sm.currentStateName === "ready" && value.calls.length === 0 && counter.hasReset())
|
|
375
|
+
this.buckets.delete(key);
|
|
376
|
+
}
|
|
377
|
+
}, 1 * 60 * 60 * 1000).unref();
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Returns a key for saving ratelimits for routes
|
|
381
|
+
* (Taken from https://github.com/abalabahaha/eris/blob/master/lib/rest/RequestHandler.js) -> I luv u abal <3
|
|
382
|
+
* @since 0.1.0
|
|
383
|
+
* @param url url to reduce to a key something like /channels/266277541646434305/messages/266277541646434305/
|
|
384
|
+
* @param method method of the request, usual http methods like get, etc.
|
|
385
|
+
* @returns reduced url: /channels/266277541646434305/messages/:id/
|
|
386
|
+
*/
|
|
387
|
+
routify(url, method) {
|
|
388
|
+
let route = url.replaceAll(routeRegex, function (match, p) {
|
|
389
|
+
return p === "channels" || p === "guilds" || p === "webhooks" ? match : `/${p}/:id`;
|
|
390
|
+
}).replaceAll(reactionsRegex, "/reactions/:id").replaceAll(reactionsUserRegex, "/reactions/:id/:userId").replace(webhooksRegex, "/webhooks/$1/:token");
|
|
391
|
+
if (method === "DELETE" && isMessageEndpointRegex.test(route))
|
|
392
|
+
route = method + route;
|
|
393
|
+
else if (method === "GET" && isGuildChannelsRegex.test(route))
|
|
394
|
+
route = "/guilds/:id/channels";
|
|
395
|
+
if (method === "PUT" || method === "DELETE") {
|
|
396
|
+
const index = route.indexOf("/reactions");
|
|
397
|
+
if (index !== -1)
|
|
398
|
+
route = "MODIFY" + route.slice(0, index + 10);
|
|
399
|
+
}
|
|
400
|
+
return route;
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* Choose a bucket from the route and enqueue a rest call in it
|
|
404
|
+
* @since 0.1.0
|
|
405
|
+
* @param fn function to call once the ratelimit is ready
|
|
406
|
+
* @param url Endpoint of the request
|
|
407
|
+
* @param method Http method used by the request
|
|
408
|
+
*/
|
|
409
|
+
queue(fn, url, method) {
|
|
410
|
+
const routeKey = this.routify(url, method);
|
|
411
|
+
let bucket = this.buckets.get(routeKey);
|
|
412
|
+
if (!bucket) {
|
|
413
|
+
if (method === "DELETE" && messagesRegex.test(url)) {
|
|
414
|
+
bucket = new Bucket([new LeakyCounter(1), new IntervalCounter(5, 5000), this.globalBucket.counters[0]]);
|
|
415
|
+
}
|
|
416
|
+
else
|
|
417
|
+
bucket = new Bucket([new LeakyCounter(1), this.globalBucket.counters[0]]);
|
|
418
|
+
this.buckets.set(routeKey, bucket);
|
|
419
|
+
}
|
|
420
|
+
return bucket.enqueue(fn);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* Set if this Ratelimiter is hitting a global ratelimit for `ms` duration
|
|
424
|
+
* @since 0.12.0
|
|
425
|
+
* @param ms How long in milliseconds this Ratelimiter is globally ratelimited for
|
|
426
|
+
*/
|
|
427
|
+
setGlobal(ms) {
|
|
428
|
+
this.globalBucket.counters[0].applyCount(Constants.GLOBAL_REQUESTS_PER_SECOND, 0, ms);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
exports.Ratelimiter = Ratelimiter;
|
|
432
|
+
/**
|
|
433
|
+
* Request Handler class
|
|
434
|
+
* @since 0.1.0
|
|
435
|
+
*/
|
|
436
|
+
class RequestHandler extends node_events_1.EventEmitter {
|
|
437
|
+
ratelimiter;
|
|
438
|
+
options;
|
|
439
|
+
latency;
|
|
440
|
+
apiURL;
|
|
441
|
+
/**
|
|
442
|
+
* Create a new request handler
|
|
443
|
+
* @param ratelimiter ratelimiter to use for ratelimiting requests
|
|
444
|
+
* @param options options
|
|
445
|
+
*/
|
|
446
|
+
constructor(ratelimiter, options) {
|
|
447
|
+
super();
|
|
448
|
+
this.ratelimiter = ratelimiter;
|
|
449
|
+
this.options = {
|
|
450
|
+
baseHost: options?.baseHost ?? Endpoints.BASE_HOST,
|
|
451
|
+
baseURL: options?.baseURL ?? Endpoints.BASE_URL,
|
|
452
|
+
bypassBuckets: options?.bypassBuckets ?? false,
|
|
453
|
+
retryFailed: options?.retryFailed ?? false,
|
|
454
|
+
retryLimit: options?.retryLimit ?? Constants.DEFAULT_RETRY_LIMIT,
|
|
455
|
+
headers: {
|
|
456
|
+
"User-Agent": `Discordbot (https://github.com/DasWolke/SnowTransfer, ${version}) Node.js/${process.version}`
|
|
457
|
+
},
|
|
458
|
+
fetch: options?.fetch ?? fetch
|
|
459
|
+
};
|
|
460
|
+
if (options?.token)
|
|
461
|
+
this.options.headers.Authorization = options.token;
|
|
462
|
+
this.apiURL = this.options.baseHost + this.options.baseURL;
|
|
463
|
+
this.latency = 500;
|
|
464
|
+
}
|
|
465
|
+
request(endpoint, params = {}, method, dataType, data, extraHeaders, retries = this.options.retryLimit, rawResponse = false) {
|
|
466
|
+
const stack = new Error("SnowTransfer dummy Error to capture before processTicksAndRejections").stack;
|
|
467
|
+
return new Promise(async (resolve, reject) => {
|
|
468
|
+
const fn = async (bkt) => {
|
|
469
|
+
const reqId = nodeCrypto.randomBytes(20).toString("hex");
|
|
470
|
+
try {
|
|
471
|
+
const request = { endpoint, method: method.toUpperCase(), dataType, data: data ?? {} };
|
|
472
|
+
this.emit("request", reqId, request);
|
|
473
|
+
const before = Date.now();
|
|
474
|
+
let response;
|
|
475
|
+
switch (dataType) {
|
|
476
|
+
case "json":
|
|
477
|
+
response = await this._request(endpoint, params, method, data, extraHeaders);
|
|
478
|
+
break;
|
|
479
|
+
case "multipart":
|
|
480
|
+
if (!data)
|
|
481
|
+
throw new Error("No multipart data");
|
|
482
|
+
response = await this._multiPartRequest(endpoint, params, method, data, extraHeaders);
|
|
483
|
+
break;
|
|
484
|
+
default:
|
|
485
|
+
throw new Error("Forbidden dataType. Use json or multipart or ensure multipart has FormData");
|
|
486
|
+
}
|
|
487
|
+
this.latency = Date.now() - before;
|
|
488
|
+
bkt?.counters.forEach(c => c.responseReceived());
|
|
489
|
+
if (bkt)
|
|
490
|
+
this._applyRatelimitHeaders(bkt, response.headers);
|
|
491
|
+
if (response.status && !Constants.OK_STATUS_CODES.has(response.status) && response.status !== 429) {
|
|
492
|
+
if (this.options.retryFailed && !Constants.DO_NOT_RETRY_STATUS_CODES.has(response.status) && retries !== 0)
|
|
493
|
+
return resolve(this.request(endpoint, params, method, dataType, data, extraHeaders, retries - 1, rawResponse ? true : undefined));
|
|
494
|
+
throw new DiscordAPIError(parseErrorBody(await response.text()), request, response);
|
|
495
|
+
}
|
|
496
|
+
if (response.status === 429) {
|
|
497
|
+
const b = await response.json(); // Discord says it will be a JSON, so if there's an error, sucks
|
|
498
|
+
if (b.global)
|
|
499
|
+
this.ratelimiter.setGlobal(b.retry_after * 1000);
|
|
500
|
+
if (globalThis.snowtransferDebugLogging)
|
|
501
|
+
console.log(`${new Date().toISOString()} [rate] [${bkt?.counters[0].id}] !! 429 - guess there was 0 remaining, wait another ${b.retry_after * 1000} (route: ${this.ratelimiter.routify(endpoint, method.toUpperCase())})`);
|
|
502
|
+
this.emit("rateLimit", {
|
|
503
|
+
method: method.toUpperCase(),
|
|
504
|
+
path: endpoint,
|
|
505
|
+
route: this.ratelimiter.routify(endpoint, method.toUpperCase())
|
|
506
|
+
});
|
|
507
|
+
if (this.options.retryFailed && retries !== 0)
|
|
508
|
+
return resolve(this.request(endpoint, params, method, dataType, data, extraHeaders, 0, rawResponse ? true : undefined));
|
|
509
|
+
throw new DiscordAPIError({ message: b.message, code: b.code ?? 429 }, request, response);
|
|
510
|
+
}
|
|
511
|
+
this.emit("done", reqId, response, request);
|
|
512
|
+
if (rawResponse)
|
|
513
|
+
return resolve(response);
|
|
514
|
+
if (response.body) {
|
|
515
|
+
let b;
|
|
516
|
+
try {
|
|
517
|
+
b = await response.json();
|
|
518
|
+
}
|
|
519
|
+
catch {
|
|
520
|
+
return resolve(undefined);
|
|
521
|
+
}
|
|
522
|
+
return resolve(b);
|
|
523
|
+
}
|
|
524
|
+
else
|
|
525
|
+
return resolve(undefined);
|
|
526
|
+
}
|
|
527
|
+
catch (error) {
|
|
528
|
+
if (error?.stack)
|
|
529
|
+
error.stack = error.stack + `\n${stack.split("\n").slice(1).join("\n")}`;
|
|
530
|
+
this.emit("requestError", reqId, error);
|
|
531
|
+
return reject(error);
|
|
532
|
+
}
|
|
533
|
+
};
|
|
534
|
+
if (this.options.bypassBuckets)
|
|
535
|
+
fn();
|
|
536
|
+
else
|
|
537
|
+
this.ratelimiter.queue(fn, endpoint, method.toUpperCase());
|
|
538
|
+
});
|
|
539
|
+
}
|
|
540
|
+
/**
|
|
541
|
+
* Apply the received ratelimit headers to the ratelimit bucket
|
|
542
|
+
* @since 0.1.0
|
|
543
|
+
* @param bkt Ratelimit bucket to apply the headers to
|
|
544
|
+
* @param headers Http headers received from discord
|
|
545
|
+
*/
|
|
546
|
+
_applyRatelimitHeaders(bkt, headers) {
|
|
547
|
+
const remaining = headers.get("x-ratelimit-remaining");
|
|
548
|
+
const limit = headers.get("x-ratelimit-limit");
|
|
549
|
+
const resetAfter = headers.get("x-ratelimit-reset-after");
|
|
550
|
+
const isGlobal = headers.get("x-ratelimit-global");
|
|
551
|
+
if (remaining === null && !bkt.counters[0].canTake() && !isGlobal) {
|
|
552
|
+
// have to reset it now, or it'll never reset again
|
|
553
|
+
bkt.counters[0].applyCount(null, 1, 0);
|
|
554
|
+
}
|
|
555
|
+
if (remaining && limit && resetAfter && !isGlobal) {
|
|
556
|
+
bkt.counters[0].applyCount(Number.parseInt(limit), Number.parseInt(remaining), Number.parseFloat(resetAfter) * 1000);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Execute a normal json request
|
|
561
|
+
* @since 0.1.0
|
|
562
|
+
* @param endpoint Endpoint to use
|
|
563
|
+
* @param params URL query parameters to add on to the URL
|
|
564
|
+
* @param data Data to send
|
|
565
|
+
* @returns Result of the request
|
|
566
|
+
*/
|
|
567
|
+
async _request(endpoint, params = {}, method, data, extraHeaders) {
|
|
568
|
+
const headers = { ...this.options.headers, ...extraHeaders };
|
|
569
|
+
let body = undefined;
|
|
570
|
+
if (data != null && !disallowedBodyMethods.has(method)) {
|
|
571
|
+
if (typeof data === "object")
|
|
572
|
+
body = JSON.stringify(data);
|
|
573
|
+
else
|
|
574
|
+
body = String(data);
|
|
575
|
+
headers["Content-Type"] = "application/json";
|
|
576
|
+
}
|
|
577
|
+
return this.options.fetch(`${this.apiURL}${endpoint}${appendQuery(params)}`, {
|
|
578
|
+
method: method.toUpperCase(),
|
|
579
|
+
headers,
|
|
580
|
+
body
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Execute a multipart/form-data request
|
|
585
|
+
* @since 0.1.0
|
|
586
|
+
* @param endpoint Endpoint to use
|
|
587
|
+
* @param params URL query parameters to add on to the URL
|
|
588
|
+
* @param method Http Method to use
|
|
589
|
+
* @param data data to send
|
|
590
|
+
* @returns Result of the request
|
|
591
|
+
*/
|
|
592
|
+
async _multiPartRequest(endpoint, params = {}, method, data, extraHeaders) {
|
|
593
|
+
const headers = { ...this.options.headers, ...extraHeaders };
|
|
594
|
+
return this.options.fetch(`${this.apiURL}${endpoint}${appendQuery(params)}`, {
|
|
595
|
+
method: method.toUpperCase(),
|
|
596
|
+
headers,
|
|
597
|
+
body: data
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
exports.RequestHandler = RequestHandler;
|
|
602
|
+
function appendQuery(query) {
|
|
603
|
+
const filtered = {};
|
|
604
|
+
let count = 0;
|
|
605
|
+
for (const [key, value] of Object.entries(query)) {
|
|
606
|
+
if (value != undefined) {
|
|
607
|
+
filtered[key] = value;
|
|
608
|
+
count++;
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
return count > 0 ? `?${new URLSearchParams(filtered).toString()}` : "";
|
|
612
|
+
}
|
|
613
|
+
/**
|
|
614
|
+
* Try to pull message/code out of a Discord JSON error body so DiscordAPIError.code is usable.
|
|
615
|
+
* Falls back to the raw text if the body isn't JSON.
|
|
616
|
+
*/
|
|
617
|
+
function parseErrorBody(text) {
|
|
618
|
+
try {
|
|
619
|
+
const body = JSON.parse(text);
|
|
620
|
+
let message = body.message ?? text;
|
|
621
|
+
// Keep the detailed validation errors visible like the raw text used to be
|
|
622
|
+
if (body.message && body.errors)
|
|
623
|
+
message = `${body.message}\n${util.inspect(body.errors, { depth: Infinity })}`;
|
|
624
|
+
return { message, code: body.code };
|
|
625
|
+
}
|
|
626
|
+
catch {
|
|
627
|
+
return { message: text };
|
|
628
|
+
}
|
|
629
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { Ratelimiter, RequestHandler } from "./RequestHandler";
|
|
2
|
+
import AssetsMethods = require("./methods/Assets");
|
|
3
|
+
import AuditLogMethods = require("./methods/AuditLog");
|
|
4
|
+
import AutoModerationMethods = require("./methods/AutoModeration");
|
|
5
|
+
import BotMethods = require("./methods/Bot");
|
|
6
|
+
import ChannelMethods = require("./methods/Channel");
|
|
7
|
+
import EntitlementMethods = require("./methods/Entitlements");
|
|
8
|
+
import GuildMethods = require("./methods/Guild");
|
|
9
|
+
import GuildScheduledEventMethods = require("./methods/GuildScheduledEvent");
|
|
10
|
+
import GuildTemplateMethods = require("./methods/GuildTemplate");
|
|
11
|
+
import InteractionMethods = require("./methods/Interaction");
|
|
12
|
+
import InviteMethods = require("./methods/Invite");
|
|
13
|
+
import SkuMethods = require("./methods/Sku");
|
|
14
|
+
import StageInstanceMethods = require("./methods/StageInstance");
|
|
15
|
+
import UserMethods = require("./methods/User");
|
|
16
|
+
import VoiceMethods = require("./methods/Voice");
|
|
17
|
+
import WebhookMethods = require("./methods/Webhook");
|
|
18
|
+
import type { SnowTransferOptions } from "./Types";
|
|
19
|
+
/**
|
|
20
|
+
* @since 0.1.0
|
|
21
|
+
*/
|
|
22
|
+
declare class SnowTransfer {
|
|
23
|
+
/** Options for this SnowTransfer instance */
|
|
24
|
+
options: SnowTransferOptions;
|
|
25
|
+
/** The access token to use for requests. Can be a bot or bearer token */
|
|
26
|
+
token: string | undefined;
|
|
27
|
+
/** Methods related to channels */
|
|
28
|
+
readonly channel: ChannelMethods;
|
|
29
|
+
/** Helper to execute REST calls */
|
|
30
|
+
readonly requestHandler: RequestHandler;
|
|
31
|
+
/** Methods related to users */
|
|
32
|
+
readonly user: UserMethods;
|
|
33
|
+
/** Methods related to stickers and emojis */
|
|
34
|
+
readonly assets: AssetsMethods;
|
|
35
|
+
/** Methods related to webhooks */
|
|
36
|
+
readonly webhook: WebhookMethods;
|
|
37
|
+
/** Methods related to guilds */
|
|
38
|
+
readonly guild: GuildMethods;
|
|
39
|
+
/** Methods related to guild scheduled events */
|
|
40
|
+
readonly guildScheduledEvent: GuildScheduledEventMethods;
|
|
41
|
+
/** Methods related to guild templates */
|
|
42
|
+
readonly guildTemplate: GuildTemplateMethods;
|
|
43
|
+
/** Methods related to application commands/interactions */
|
|
44
|
+
readonly interaction: InteractionMethods;
|
|
45
|
+
/** Methods related to invites */
|
|
46
|
+
readonly invite: InviteMethods;
|
|
47
|
+
/** Methods related to voice regions */
|
|
48
|
+
readonly voice: VoiceMethods;
|
|
49
|
+
/** Methods related to getting gateway connect info */
|
|
50
|
+
readonly bot: BotMethods;
|
|
51
|
+
/** Methods related to guild audit logs */
|
|
52
|
+
readonly auditLog: AuditLogMethods;
|
|
53
|
+
/** Methods related to guild stage instances */
|
|
54
|
+
readonly stageInstance: StageInstanceMethods;
|
|
55
|
+
/** Methods related to guild auto mod */
|
|
56
|
+
readonly autoMod: AutoModerationMethods;
|
|
57
|
+
/** Methods related to entitlements */
|
|
58
|
+
readonly entitlement: EntitlementMethods;
|
|
59
|
+
/** Methods related to SKUs */
|
|
60
|
+
readonly sku: SkuMethods;
|
|
61
|
+
/** Ratelimiter used for handling the ratelimits imposed by the rest api */
|
|
62
|
+
readonly ratelimiter: Ratelimiter;
|
|
63
|
+
/**
|
|
64
|
+
* Create a new Rest Client
|
|
65
|
+
* @param token Discord Bot token to use
|
|
66
|
+
* @param options options
|
|
67
|
+
*/
|
|
68
|
+
constructor(token?: string, options?: Partial<SnowTransferOptions>);
|
|
69
|
+
}
|
|
70
|
+
export = SnowTransfer;
|