@discordkit/core 3.2.0 → 4.0.1
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 +423 -0
- package/README.md +52 -0
- package/dist/index.d.mts +23 -0
- package/dist/index.mjs +23 -0
- package/dist/requests/DiscordSession.d.mts +72 -0
- package/dist/requests/DiscordSession.d.mts.map +1 -0
- package/dist/requests/DiscordSession.mjs +262 -0
- package/dist/requests/DiscordSession.mjs.map +1 -0
- package/dist/requests/addParams.d.mts +16 -0
- package/dist/requests/addParams.d.mts.map +1 -0
- package/dist/requests/addParams.mjs +26 -0
- package/dist/requests/addParams.mjs.map +1 -0
- package/dist/requests/buildURL.d.mts +8 -0
- package/dist/requests/buildURL.d.mts.map +1 -0
- package/dist/requests/buildURL.mjs +9 -0
- package/dist/requests/buildURL.mjs.map +1 -0
- package/dist/requests/getAsset.d.mts +8 -0
- package/dist/requests/getAsset.d.mts.map +1 -0
- package/dist/requests/getAsset.mjs +8 -0
- package/dist/requests/getAsset.mjs.map +1 -0
- package/dist/requests/index.d.mts +9 -0
- package/dist/requests/index.mjs +9 -0
- package/dist/requests/methods.d.mts +64 -0
- package/dist/requests/methods.d.mts.map +1 -0
- package/dist/requests/methods.mjs +12 -0
- package/dist/requests/methods.mjs.map +1 -0
- package/dist/requests/request.d.mts +28 -0
- package/dist/requests/request.d.mts.map +1 -0
- package/dist/requests/request.mjs +31 -0
- package/dist/requests/request.mjs.map +1 -0
- package/dist/requests/toProcedure.d.mts +41 -0
- package/dist/requests/toProcedure.d.mts.map +1 -0
- package/dist/requests/toProcedure.mjs +29 -0
- package/dist/requests/toProcedure.mjs.map +1 -0
- package/dist/requests/toQuery.d.mts +37 -0
- package/dist/requests/toQuery.d.mts.map +1 -0
- package/dist/requests/toQuery.mjs +10 -0
- package/dist/requests/toQuery.mjs.map +1 -0
- package/dist/requests/toValidated.d.mts +17 -0
- package/dist/requests/toValidated.d.mts.map +1 -0
- package/dist/requests/toValidated.mjs +27 -0
- package/dist/requests/toValidated.mjs.map +1 -0
- package/dist/requests/{verifyKey.d.ts → verifyKey.d.mts} +5 -1
- package/dist/requests/verifyKey.d.mts.map +1 -0
- package/dist/requests/verifyKey.mjs +64 -0
- package/dist/requests/verifyKey.mjs.map +1 -0
- package/dist/utils/isBetween.d.mts +5 -0
- package/dist/utils/isBetween.d.mts.map +1 -0
- package/dist/utils/isBetween.mjs +6 -0
- package/dist/utils/isBetween.mjs.map +1 -0
- package/dist/utils/{isNonNullable.js → isNonNullable.d.mts} +6 -2
- package/dist/utils/isNonNullable.d.mts.map +1 -0
- package/dist/utils/isNonNullable.mjs +24 -0
- package/dist/utils/isNonNullable.mjs.map +1 -0
- package/dist/utils/isNumericString.d.mts +5 -0
- package/dist/utils/isNumericString.d.mts.map +1 -0
- package/dist/utils/isNumericString.mjs +6 -0
- package/dist/utils/isNumericString.mjs.map +1 -0
- package/dist/utils/isObject.d.mts +5 -0
- package/dist/utils/isObject.d.mts.map +1 -0
- package/dist/utils/isObject.mjs +6 -0
- package/dist/utils/isObject.mjs.map +1 -0
- package/dist/utils/sleep.d.mts +8 -0
- package/dist/utils/sleep.d.mts.map +1 -0
- package/dist/utils/sleep.mjs +9 -0
- package/dist/utils/sleep.mjs.map +1 -0
- package/dist/utils/toCamelCase.d.mts +5 -0
- package/dist/utils/toCamelCase.d.mts.map +1 -0
- package/dist/utils/toCamelCase.mjs +6 -0
- package/dist/utils/toCamelCase.mjs.map +1 -0
- package/dist/utils/toCamelKeys.d.mts +20 -0
- package/dist/utils/toCamelKeys.d.mts.map +1 -0
- package/dist/utils/toCamelKeys.mjs +15 -0
- package/dist/utils/toCamelKeys.mjs.map +1 -0
- package/dist/utils/toSnakeCase.d.mts +5 -0
- package/dist/utils/toSnakeCase.d.mts.map +1 -0
- package/dist/utils/toSnakeCase.mjs +6 -0
- package/dist/utils/toSnakeCase.mjs.map +1 -0
- package/dist/utils/toSnakeKeys.d.mts +24 -0
- package/dist/utils/toSnakeKeys.d.mts.map +1 -0
- package/dist/utils/toSnakeKeys.mjs +15 -0
- package/dist/utils/toSnakeKeys.mjs.map +1 -0
- package/dist/validations/asDigits.d.mts +13 -0
- package/dist/validations/asDigits.d.mts.map +1 -0
- package/dist/validations/asDigits.mjs +12 -0
- package/dist/validations/asDigits.mjs.map +1 -0
- package/dist/validations/asInteger.d.mts +13 -0
- package/dist/validations/asInteger.d.mts.map +1 -0
- package/dist/validations/asInteger.mjs +12 -0
- package/dist/validations/asInteger.mjs.map +1 -0
- package/dist/validations/bitfield.d.mts +24 -0
- package/dist/validations/bitfield.d.mts.map +1 -0
- package/dist/validations/bitfield.mjs +22 -0
- package/dist/validations/bitfield.mjs.map +1 -0
- package/dist/validations/boundedArray.d.mts +14 -0
- package/dist/validations/boundedArray.d.mts.map +1 -0
- package/dist/validations/boundedArray.mjs +11 -0
- package/dist/validations/boundedArray.mjs.map +1 -0
- package/dist/validations/boundedInteger.d.mts +14 -0
- package/dist/validations/boundedInteger.d.mts.map +1 -0
- package/dist/validations/boundedInteger.mjs +11 -0
- package/dist/validations/boundedInteger.mjs.map +1 -0
- package/dist/validations/boundedString.d.mts +15 -0
- package/dist/validations/boundedString.d.mts.map +1 -0
- package/dist/validations/boundedString.mjs +12 -0
- package/dist/validations/boundedString.mjs.map +1 -0
- package/dist/validations/datauri.d.mts +25 -0
- package/dist/validations/datauri.d.mts.map +1 -0
- package/dist/validations/datauri.mjs +19 -0
- package/dist/validations/datauri.mjs.map +1 -0
- package/dist/validations/fileUpload.d.mts +130 -0
- package/dist/validations/fileUpload.d.mts.map +1 -0
- package/dist/validations/fileUpload.mjs +116 -0
- package/dist/validations/fileUpload.mjs.map +1 -0
- package/dist/validations/hasMimeType.d.mts +17 -0
- package/dist/validations/hasMimeType.d.mts.map +1 -0
- package/dist/validations/hasMimeType.mjs +18 -0
- package/dist/validations/hasMimeType.mjs.map +1 -0
- package/dist/validations/hasSize.d.mts +11 -0
- package/dist/validations/hasSize.d.mts.map +1 -0
- package/dist/validations/hasSize.mjs +14 -0
- package/dist/validations/hasSize.mjs.map +1 -0
- package/dist/validations/index.d.mts +15 -0
- package/dist/validations/index.mjs +15 -0
- package/dist/validations/schema.d.mts +103 -0
- package/dist/validations/schema.d.mts.map +1 -0
- package/dist/validations/schema.mjs +111 -0
- package/dist/validations/schema.mjs.map +1 -0
- package/dist/validations/{snowflake.d.ts → snowflake.d.mts} +10 -7
- package/dist/validations/snowflake.d.mts.map +1 -0
- package/dist/validations/snowflake.mjs +30 -0
- package/dist/validations/snowflake.mjs.map +1 -0
- package/dist/validations/timestamp.d.mts +8 -0
- package/dist/validations/timestamp.d.mts.map +1 -0
- package/dist/validations/timestamp.mjs +8 -0
- package/dist/validations/timestamp.mjs.map +1 -0
- package/dist/validations/toBlob.d.mts +8 -0
- package/dist/validations/toBlob.d.mts.map +1 -0
- package/dist/validations/toBlob.mjs +19 -0
- package/dist/validations/toBlob.mjs.map +1 -0
- package/dist/validations/url.d.mts +7 -0
- package/dist/validations/url.d.mts.map +1 -0
- package/dist/validations/url.mjs +7 -0
- package/dist/validations/url.mjs.map +1 -0
- package/package.json +10 -23
- package/dist/index.d.ts +0 -2
- package/dist/index.js +0 -3
- package/dist/index.js.map +0 -1
- package/dist/requests/DiscordSession.d.ts +0 -25
- package/dist/requests/DiscordSession.js +0 -255
- package/dist/requests/DiscordSession.js.map +0 -1
- package/dist/requests/addParams.d.ts +0 -2
- package/dist/requests/addParams.js +0 -11
- package/dist/requests/addParams.js.map +0 -1
- package/dist/requests/buildURL.d.ts +0 -2
- package/dist/requests/buildURL.js +0 -4
- package/dist/requests/buildURL.js.map +0 -1
- package/dist/requests/getAsset.d.ts +0 -2
- package/dist/requests/getAsset.js +0 -3
- package/dist/requests/getAsset.js.map +0 -1
- package/dist/requests/index.d.ts +0 -8
- package/dist/requests/index.js +0 -9
- package/dist/requests/index.js.map +0 -1
- package/dist/requests/methods.d.ts +0 -13
- package/dist/requests/methods.js +0 -8
- package/dist/requests/methods.js.map +0 -1
- package/dist/requests/request.d.ts +0 -2
- package/dist/requests/request.js +0 -30
- package/dist/requests/request.js.map +0 -1
- package/dist/requests/toProcedure.d.ts +0 -31
- package/dist/requests/toProcedure.js +0 -36
- package/dist/requests/toProcedure.js.map +0 -1
- package/dist/requests/toQuery.d.ts +0 -28
- package/dist/requests/toQuery.js +0 -9
- package/dist/requests/toQuery.js.map +0 -1
- package/dist/requests/toValidated.d.ts +0 -13
- package/dist/requests/toValidated.js +0 -29
- package/dist/requests/toValidated.js.map +0 -1
- package/dist/requests/verifyKey.js +0 -91
- package/dist/requests/verifyKey.js.map +0 -1
- package/dist/utils/isBetween.d.ts +0 -1
- package/dist/utils/isBetween.js +0 -2
- package/dist/utils/isBetween.js.map +0 -1
- package/dist/utils/isNonNullable.d.ts +0 -20
- package/dist/utils/isNonNullable.js.map +0 -1
- package/dist/utils/isNumericString.d.ts +0 -1
- package/dist/utils/isNumericString.js +0 -2
- package/dist/utils/isNumericString.js.map +0 -1
- package/dist/utils/isObject.d.ts +0 -1
- package/dist/utils/isObject.js +0 -2
- package/dist/utils/isObject.js.map +0 -1
- package/dist/utils/sleep.d.ts +0 -4
- package/dist/utils/sleep.js +0 -5
- package/dist/utils/sleep.js.map +0 -1
- package/dist/utils/toCamelCase.d.ts +0 -1
- package/dist/utils/toCamelCase.js +0 -2
- package/dist/utils/toCamelCase.js.map +0 -1
- package/dist/utils/toCamelKeys.d.ts +0 -2
- package/dist/utils/toCamelKeys.js +0 -16
- package/dist/utils/toCamelKeys.js.map +0 -1
- package/dist/utils/toSnakeCase.d.ts +0 -1
- package/dist/utils/toSnakeCase.js +0 -4
- package/dist/utils/toSnakeCase.js.map +0 -1
- package/dist/utils/toSnakeKeys.d.ts +0 -2
- package/dist/utils/toSnakeKeys.js +0 -16
- package/dist/utils/toSnakeKeys.js.map +0 -1
- package/dist/validations/asDigits.d.ts +0 -6
- package/dist/validations/asDigits.js +0 -6
- package/dist/validations/asDigits.js.map +0 -1
- package/dist/validations/asInteger.d.ts +0 -6
- package/dist/validations/asInteger.js +0 -6
- package/dist/validations/asInteger.js.map +0 -1
- package/dist/validations/bitfield.d.ts +0 -17
- package/dist/validations/bitfield.js +0 -37
- package/dist/validations/bitfield.js.map +0 -1
- package/dist/validations/boundedArray.d.ts +0 -6
- package/dist/validations/boundedArray.js +0 -8
- package/dist/validations/boundedArray.js.map +0 -1
- package/dist/validations/boundedInteger.d.ts +0 -6
- package/dist/validations/boundedInteger.js +0 -8
- package/dist/validations/boundedInteger.js.map +0 -1
- package/dist/validations/boundedString.d.ts +0 -6
- package/dist/validations/boundedString.js +0 -8
- package/dist/validations/boundedString.js.map +0 -1
- package/dist/validations/datauri.d.ts +0 -20
- package/dist/validations/datauri.js +0 -20
- package/dist/validations/datauri.js.map +0 -1
- package/dist/validations/hasMimeType.d.ts +0 -10
- package/dist/validations/hasMimeType.js +0 -18
- package/dist/validations/hasMimeType.js.map +0 -1
- package/dist/validations/hasSize.d.ts +0 -5
- package/dist/validations/hasSize.js +0 -13
- package/dist/validations/hasSize.js.map +0 -1
- package/dist/validations/index.d.ts +0 -12
- package/dist/validations/index.js +0 -13
- package/dist/validations/index.js.map +0 -1
- package/dist/validations/snowflake.js +0 -39
- package/dist/validations/snowflake.js.map +0 -1
- package/dist/validations/timestamp.d.ts +0 -3
- package/dist/validations/timestamp.js +0 -4
- package/dist/validations/timestamp.js.map +0 -1
- package/dist/validations/toBlob.d.ts +0 -4
- package/dist/validations/toBlob.js +0 -19
- package/dist/validations/toBlob.js.map +0 -1
- package/dist/validations/url.d.ts +0 -2
- package/dist/validations/url.js +0 -3
- package/dist/validations/url.js.map +0 -1
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { sleep } from "../utils/sleep.mjs";
|
|
2
|
+
//#region src/requests/DiscordSession.ts
|
|
3
|
+
const endpoint = `https://discord.com/api/v10/`;
|
|
4
|
+
/** @internal */
|
|
5
|
+
var DiscordSession = class {
|
|
6
|
+
endpoint = endpoint;
|
|
7
|
+
maxRetries = 5;
|
|
8
|
+
#authToken = null;
|
|
9
|
+
/**
|
|
10
|
+
* The currently-active per-request token override. When set, requests
|
|
11
|
+
* enqueued via {@link queueRequest} capture this value (instead of the
|
|
12
|
+
* session token) for their `Authorization` header. Used to make user-scoped
|
|
13
|
+
* (OAuth2 bearer) calls without permanently changing the session.
|
|
14
|
+
*/
|
|
15
|
+
#activeToken = null;
|
|
16
|
+
#buckets = /* @__PURE__ */ new Map();
|
|
17
|
+
#globalReset = 0;
|
|
18
|
+
#requestQueue = [];
|
|
19
|
+
#processingQueue = false;
|
|
20
|
+
#globalLimit = 50;
|
|
21
|
+
#globalWindow = 1e3;
|
|
22
|
+
#globalRequestTimestamps = [];
|
|
23
|
+
#invalidRequests = {
|
|
24
|
+
count: 0,
|
|
25
|
+
windowStart: Date.now()
|
|
26
|
+
};
|
|
27
|
+
#invalidRequestLimit = 1e4;
|
|
28
|
+
#invalidRequestWindow = 600 * 1e3;
|
|
29
|
+
get ready() {
|
|
30
|
+
return Boolean(this.#authToken);
|
|
31
|
+
}
|
|
32
|
+
constructor(authToken) {
|
|
33
|
+
if (authToken) this.setToken(authToken);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Clears the current session details, then attempts to set
|
|
37
|
+
* new session values
|
|
38
|
+
*/
|
|
39
|
+
setToken = (token) => {
|
|
40
|
+
this.#authToken = null;
|
|
41
|
+
if (token.length === 0) throw new Error(`Must provide a non-empty string to set Auth Token`);
|
|
42
|
+
if (!token.startsWith(`Bot `) && !token.startsWith(`Bearer `)) throw new Error(`Token must begin with either "Bot " or "Bearer ", received: ${token}`);
|
|
43
|
+
this.#authToken = token;
|
|
44
|
+
return this;
|
|
45
|
+
};
|
|
46
|
+
clearSession = () => {
|
|
47
|
+
this.#authToken = null;
|
|
48
|
+
this.#activeToken = null;
|
|
49
|
+
this.#buckets.clear();
|
|
50
|
+
this.#globalReset = 0;
|
|
51
|
+
this.#requestQueue = [];
|
|
52
|
+
this.#globalRequestTimestamps = [];
|
|
53
|
+
this.#invalidRequests = {
|
|
54
|
+
count: 0,
|
|
55
|
+
windowStart: Date.now()
|
|
56
|
+
};
|
|
57
|
+
return this;
|
|
58
|
+
};
|
|
59
|
+
/** Clear the active per-request token; requests fall back to the session token. */
|
|
60
|
+
clearActiveToken = () => {
|
|
61
|
+
this.#activeToken = null;
|
|
62
|
+
return this;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Scope subsequent requests to a single user's OAuth2 access token.
|
|
66
|
+
*
|
|
67
|
+
* Pass the **bare** access token (no `Bearer ` prefix — it's added for you).
|
|
68
|
+
* Returns a disposable handle whose `request()` method runs a callback with
|
|
69
|
+
* the user's token active, so any discordkit fetcher called inside it
|
|
70
|
+
* authenticates as that user instead of the bot session:
|
|
71
|
+
*
|
|
72
|
+
* ```ts
|
|
73
|
+
* using user = discord.asUser(accessToken);
|
|
74
|
+
* const guilds = await user.request(() => getCurrentUserGuilds({}));
|
|
75
|
+
* // `using` disposes the handle at scope exit — even on throw — clearing
|
|
76
|
+
* // the active token so it can't leak into a later request (important on
|
|
77
|
+
* // reused serverless/warm instances).
|
|
78
|
+
* ```
|
|
79
|
+
*
|
|
80
|
+
* Without `using`, call {@link clearActiveToken} yourself when done, or rely
|
|
81
|
+
* on each `request()` restoring the previous token after it resolves.
|
|
82
|
+
*/
|
|
83
|
+
asUser = (accessToken) => {
|
|
84
|
+
const token = `Bearer ${accessToken}`;
|
|
85
|
+
return {
|
|
86
|
+
request: async (fn) => {
|
|
87
|
+
const previous = this.#activeToken;
|
|
88
|
+
this.#activeToken = token;
|
|
89
|
+
try {
|
|
90
|
+
return await fn();
|
|
91
|
+
} finally {
|
|
92
|
+
this.#activeToken = previous;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
[Symbol.dispose]: () => {
|
|
96
|
+
this.#activeToken = null;
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
};
|
|
100
|
+
/**
|
|
101
|
+
* Whether a request can authenticate right now — either an active
|
|
102
|
+
* per-request token or a session token is set. Used by the request layer to
|
|
103
|
+
* decide if it must fail early for lack of auth.
|
|
104
|
+
*/
|
|
105
|
+
get hasAuth() {
|
|
106
|
+
return this.#activeToken !== null || this.#authToken !== null;
|
|
107
|
+
}
|
|
108
|
+
getSession = () => {
|
|
109
|
+
const token = this.#authToken;
|
|
110
|
+
if (!token) throw new Error(`Auth Token must be set before requests can be made.`);
|
|
111
|
+
return token;
|
|
112
|
+
};
|
|
113
|
+
/**
|
|
114
|
+
* Queue a request to be processed with rate limiting
|
|
115
|
+
*/
|
|
116
|
+
queueRequest = async (resource, method, body, options) => {
|
|
117
|
+
return new Promise((resolve, reject) => {
|
|
118
|
+
this.#requestQueue.push({
|
|
119
|
+
resource,
|
|
120
|
+
method,
|
|
121
|
+
body,
|
|
122
|
+
options,
|
|
123
|
+
...this.#activeToken === null ? {} : { token: this.#activeToken },
|
|
124
|
+
resolve,
|
|
125
|
+
reject
|
|
126
|
+
});
|
|
127
|
+
this.#processQueue();
|
|
128
|
+
});
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Process the request queue with rate limiting
|
|
132
|
+
*/
|
|
133
|
+
#processQueue = async () => {
|
|
134
|
+
if (this.#processingQueue) return;
|
|
135
|
+
this.#processingQueue = true;
|
|
136
|
+
while (this.#requestQueue.length > 0) {
|
|
137
|
+
const now = Date.now();
|
|
138
|
+
if (this.#isTemporarilyBanned()) {
|
|
139
|
+
const waitTime = this.#invalidRequestWindow - (now - this.#invalidRequests.windowStart);
|
|
140
|
+
console.warn(`Temporarily banned from Discord API. Waiting ${Math.ceil(waitTime / 1e3)}s`);
|
|
141
|
+
await sleep(waitTime);
|
|
142
|
+
this.#resetInvalidRequestCounter();
|
|
143
|
+
}
|
|
144
|
+
await this.#enforceGlobalRateLimit();
|
|
145
|
+
if (this.#globalReset > now / 1e3) {
|
|
146
|
+
await sleep((this.#globalReset - now / 1e3) * 1e3);
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const request = this.#requestQueue[0];
|
|
150
|
+
if (typeof request === `undefined`) break;
|
|
151
|
+
const bucket = this.#buckets.get(`${request.method}:${request.resource}`);
|
|
152
|
+
if (bucket?.remaining === 0) {
|
|
153
|
+
const resetTime = bucket.reset * 1e3;
|
|
154
|
+
if (resetTime > now) await sleep(resetTime - now);
|
|
155
|
+
}
|
|
156
|
+
this.#requestQueue.shift();
|
|
157
|
+
try {
|
|
158
|
+
const response = await this.#executeRequest(request);
|
|
159
|
+
request.resolve(response);
|
|
160
|
+
} catch (error) {
|
|
161
|
+
if (error instanceof Error) request.reject(error);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
this.#processingQueue = false;
|
|
165
|
+
if (this.#requestQueue.length > 0) this.#processQueue();
|
|
166
|
+
};
|
|
167
|
+
/**
|
|
168
|
+
* Execute a single request and handle rate limit headers
|
|
169
|
+
*/
|
|
170
|
+
#executeRequest = async (request, retryCount = 0) => {
|
|
171
|
+
const now = Date.now();
|
|
172
|
+
this.#globalRequestTimestamps.push(now);
|
|
173
|
+
const headers = {};
|
|
174
|
+
if (!request.options?.anonymous) headers.Authorization = request.token ?? this.getSession();
|
|
175
|
+
if (request.options?.reason) headers[`X-Audit-Log-Reason`] = encodeURIComponent(request.options.reason);
|
|
176
|
+
if (request.body && !(request.body instanceof FormData)) headers[`Content-Type`] = `application/json`;
|
|
177
|
+
const response = await fetch(request.resource.toString(), {
|
|
178
|
+
method: request.method,
|
|
179
|
+
body: request.body,
|
|
180
|
+
headers
|
|
181
|
+
});
|
|
182
|
+
this.#updateRateLimits(response);
|
|
183
|
+
if (response.status === 429) {
|
|
184
|
+
if (retryCount >= this.maxRetries) {
|
|
185
|
+
console.error(`Max retries (${this.maxRetries}) exceeded for ${request.resource}`);
|
|
186
|
+
return response;
|
|
187
|
+
}
|
|
188
|
+
const retryAfter = response.headers.get(`Retry-After`) ?? `1`;
|
|
189
|
+
const isGlobal = response.headers.get(`X-RateLimit-Global`) === `true`;
|
|
190
|
+
const scope = response.headers.get(`X-RateLimit-Scope`);
|
|
191
|
+
if (isGlobal || scope === `global`) {
|
|
192
|
+
console.warn(`Hit global rate limit`);
|
|
193
|
+
this.#globalReset = now / 1e3 + parseFloat(retryAfter);
|
|
194
|
+
}
|
|
195
|
+
await sleep(parseFloat(retryAfter) * 1e3);
|
|
196
|
+
return this.#executeRequest(request, retryCount + 1);
|
|
197
|
+
}
|
|
198
|
+
if ([401, 403].includes(response.status) || response.status === 429 && response.headers.get(`X-RateLimit-Scope`) !== `shared`) this.#trackInvalidRequest();
|
|
199
|
+
return response;
|
|
200
|
+
};
|
|
201
|
+
/**
|
|
202
|
+
* Update rate limit buckets from response headers
|
|
203
|
+
*/
|
|
204
|
+
#updateRateLimits = (response) => {
|
|
205
|
+
const bucket = response.headers.get(`X-RateLimit-Bucket`);
|
|
206
|
+
const limit = response.headers.get(`X-RateLimit-Limit`);
|
|
207
|
+
const remaining = response.headers.get(`X-RateLimit-Remaining`);
|
|
208
|
+
const reset = response.headers.get(`X-RateLimit-Reset`);
|
|
209
|
+
const resetAfter = response.headers.get(`X-RateLimit-Reset-After`);
|
|
210
|
+
if (bucket && limit && remaining && reset && resetAfter) this.#buckets.set(bucket, {
|
|
211
|
+
limit: parseInt(limit, 10),
|
|
212
|
+
remaining: parseInt(remaining, 10),
|
|
213
|
+
reset: parseFloat(reset),
|
|
214
|
+
resetAfter: parseFloat(resetAfter)
|
|
215
|
+
});
|
|
216
|
+
};
|
|
217
|
+
/**
|
|
218
|
+
* Enforce global rate limit of 50 requests per second
|
|
219
|
+
*/
|
|
220
|
+
#enforceGlobalRateLimit = async () => {
|
|
221
|
+
const now = Date.now();
|
|
222
|
+
this.#globalRequestTimestamps = this.#globalRequestTimestamps.filter((timestamp) => now - timestamp < this.#globalWindow);
|
|
223
|
+
if (this.#globalRequestTimestamps.length >= this.#globalLimit) {
|
|
224
|
+
const oldestTimestamp = this.#globalRequestTimestamps[0];
|
|
225
|
+
const waitTime = this.#globalWindow - (now - oldestTimestamp);
|
|
226
|
+
if (waitTime > 0) await sleep(waitTime);
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
/**
|
|
230
|
+
* Track invalid requests for Cloudflare ban prevention
|
|
231
|
+
*/
|
|
232
|
+
#trackInvalidRequest = () => {
|
|
233
|
+
if (Date.now() - this.#invalidRequests.windowStart >= this.#invalidRequestWindow) this.#resetInvalidRequestCounter();
|
|
234
|
+
this.#invalidRequests.count++;
|
|
235
|
+
if (this.#invalidRequests.count >= this.#invalidRequestLimit) console.error(`Approaching invalid request limit! Bot may be temporarily banned.`);
|
|
236
|
+
};
|
|
237
|
+
/**
|
|
238
|
+
* Check if we're temporarily banned
|
|
239
|
+
*/
|
|
240
|
+
#isTemporarilyBanned = () => {
|
|
241
|
+
if (Date.now() - this.#invalidRequests.windowStart < this.#invalidRequestWindow) return this.#invalidRequests.count >= this.#invalidRequestLimit;
|
|
242
|
+
return false;
|
|
243
|
+
};
|
|
244
|
+
/**
|
|
245
|
+
* Reset invalid request counter
|
|
246
|
+
*/
|
|
247
|
+
#resetInvalidRequestCounter = () => {
|
|
248
|
+
this.#invalidRequests = {
|
|
249
|
+
count: 0,
|
|
250
|
+
windowStart: Date.now()
|
|
251
|
+
};
|
|
252
|
+
};
|
|
253
|
+
/**
|
|
254
|
+
* Get current queue size (useful for monitoring)
|
|
255
|
+
*/
|
|
256
|
+
getQueueSize = () => this.#requestQueue.length;
|
|
257
|
+
};
|
|
258
|
+
const discord = new DiscordSession();
|
|
259
|
+
//#endregion
|
|
260
|
+
export { DiscordSession, discord, endpoint };
|
|
261
|
+
|
|
262
|
+
//# sourceMappingURL=DiscordSession.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"DiscordSession.mjs","names":["#authToken","#activeToken","#buckets","#globalReset","#requestQueue","#globalRequestTimestamps","#invalidRequests","#processQueue","#processingQueue","#isTemporarilyBanned","#invalidRequestWindow","#resetInvalidRequestCounter","#enforceGlobalRateLimit","#executeRequest","#updateRateLimits","#trackInvalidRequest","#globalWindow","#globalLimit","#invalidRequestLimit"],"sources":["../../src/requests/DiscordSession.ts"],"sourcesContent":["import { sleep } from \"../utils/sleep.js\";\nimport type { RequestOptions } from \"./request.js\";\n\nexport const endpoint = `https://discord.com/api/v10/`;\n\ninterface QueuedRequest {\n resource: URL;\n method: string;\n body?: string | FormData | null;\n options?: RequestOptions;\n /**\n * The `Authorization` header value captured for this request at enqueue\n * time — a snapshot of {@link DiscordSession.setActiveToken} (falling back\n * to the session token). Travels with the request through retries so a\n * later `setActiveToken` by a concurrent caller can't change the auth used\n * for an already-sent request.\n */\n token?: `${`Bot` | `Bearer`} ${string}`;\n resolve: (value: Response) => void;\n reject: (error: Error) => void;\n retryCount?: number; // Optional retry tracking\n}\n\ninterface RateLimitBucket {\n limit: number;\n remaining: number;\n reset: number; // Epoch timestamp in seconds\n resetAfter: number; // Seconds until reset\n}\n\ninterface InvalidRequestTracker {\n count: number;\n windowStart: number; // Timestamp in ms\n}\n\n/**\n * A disposable handle scoping requests to a single user's token, returned by\n * {@link DiscordSession.asUser}. Use with `using` for automatic cleanup:\n *\n * ```ts\n * using user = discord.asUser(accessToken);\n * const me = await user.request(() => getCurrentUser());\n * ```\n */\nexport interface UserSession extends Disposable {\n /** Run `fn` with this user's token active; restores the previous token after. */\n request: <T>(fn: () => Promise<T>) => Promise<T>;\n}\n\n/** @internal */\nexport class DiscordSession {\n endpoint: string = endpoint;\n maxRetries: number = 5;\n #authToken: string | null = null;\n /**\n * The currently-active per-request token override. When set, requests\n * enqueued via {@link queueRequest} capture this value (instead of the\n * session token) for their `Authorization` header. Used to make user-scoped\n * (OAuth2 bearer) calls without permanently changing the session.\n */\n #activeToken: `${`Bot` | `Bearer`} ${string}` | null = null;\n\n // Rate limit tracking\n #buckets = new Map<string, RateLimitBucket>();\n #globalReset: number = 0; // Epoch timestamp when global limit resets\n #requestQueue: QueuedRequest[] = [];\n #processingQueue = false;\n\n // Global rate limit: 50 requests per second\n #globalLimit = 50;\n #globalWindow = 1000; // 1 second in ms\n #globalRequestTimestamps: number[] = [];\n\n // Invalid request tracking: 10,000 per 10 minutes\n #invalidRequests: InvalidRequestTracker = {\n count: 0,\n windowStart: Date.now()\n };\n\n #invalidRequestLimit = 10000;\n #invalidRequestWindow = 10 * 60 * 1000; // 10 minutes in ms\n\n get ready(): boolean {\n return Boolean(this.#authToken);\n }\n\n constructor(authToken?: `${`Bot` | `Bearer`} ${string}` | null) {\n if (authToken) {\n this.setToken(authToken);\n }\n }\n\n /**\n * Clears the current session details, then attempts to set\n * new session values\n */\n setToken = (token: `${`Bot` | `Bearer`} ${string}`): this => {\n this.#authToken = null;\n if (token.length === 0) {\n throw new Error(`Must provide a non-empty string to set Auth Token`);\n }\n if (!token.startsWith(`Bot `) && !token.startsWith(`Bearer `)) {\n throw new Error(\n `Token must begin with either \"Bot \" or \"Bearer \", received: ${token}`\n );\n }\n this.#authToken = token;\n return this;\n };\n\n clearSession = (): this => {\n this.#authToken = null;\n this.#activeToken = null;\n this.#buckets.clear();\n this.#globalReset = 0;\n this.#requestQueue = [];\n this.#globalRequestTimestamps = [];\n this.#invalidRequests = {\n count: 0,\n windowStart: Date.now()\n };\n return this;\n };\n\n /** Clear the active per-request token; requests fall back to the session token. */\n clearActiveToken = (): this => {\n this.#activeToken = null;\n return this;\n };\n\n /**\n * Scope subsequent requests to a single user's OAuth2 access token.\n *\n * Pass the **bare** access token (no `Bearer ` prefix — it's added for you).\n * Returns a disposable handle whose `request()` method runs a callback with\n * the user's token active, so any discordkit fetcher called inside it\n * authenticates as that user instead of the bot session:\n *\n * ```ts\n * using user = discord.asUser(accessToken);\n * const guilds = await user.request(() => getCurrentUserGuilds({}));\n * // `using` disposes the handle at scope exit — even on throw — clearing\n * // the active token so it can't leak into a later request (important on\n * // reused serverless/warm instances).\n * ```\n *\n * Without `using`, call {@link clearActiveToken} yourself when done, or rely\n * on each `request()` restoring the previous token after it resolves.\n */\n asUser = (accessToken: string): UserSession => {\n const token = `Bearer ${accessToken}` as const;\n return {\n request: async <T>(fn: () => Promise<T>): Promise<T> => {\n const previous = this.#activeToken;\n this.#activeToken = token;\n try {\n // `fn` enqueues the request synchronously, capturing `token` onto the\n // QueuedRequest before the first await — so restoring below can't\n // affect the in-flight request's auth.\n return await fn();\n } finally {\n this.#activeToken = previous;\n }\n },\n [Symbol.dispose]: (): void => {\n this.#activeToken = null;\n }\n };\n };\n\n /**\n * Whether a request can authenticate right now — either an active\n * per-request token or a session token is set. Used by the request layer to\n * decide if it must fail early for lack of auth.\n */\n get hasAuth(): boolean {\n return this.#activeToken !== null || this.#authToken !== null;\n }\n\n getSession = (): string => {\n const token = this.#authToken;\n\n if (!token) {\n throw new Error(`Auth Token must be set before requests can be made.`);\n }\n\n return token;\n };\n\n /**\n * Queue a request to be processed with rate limiting\n */\n queueRequest = async (\n resource: URL,\n method: string,\n body?: string | FormData | null,\n options?: RequestOptions\n ): Promise<Response> => {\n return new Promise((resolve, reject) => {\n this.#requestQueue.push({\n resource,\n method,\n body,\n options,\n // Snapshot the active token NOW, at enqueue time. Reading it here\n // (synchronously) rather than later in #executeRequest is what makes\n // concurrent per-user calls safe: a subsequent setActiveToken can't\n // retroactively change the auth for a request already in the queue.\n ...(this.#activeToken === null ? {} : { token: this.#activeToken }),\n resolve,\n reject\n });\n\n // Start processing if not already running\n void this.#processQueue(); // This will return early if already processing\n });\n };\n\n /**\n * Process the request queue with rate limiting\n */\n #processQueue = async (): Promise<void> => {\n if (this.#processingQueue) return;\n this.#processingQueue = true;\n\n while (this.#requestQueue.length > 0) {\n const now = Date.now();\n\n // Check if we're temporarily banned (invalid request limit)\n if (this.#isTemporarilyBanned()) {\n const waitTime =\n this.#invalidRequestWindow -\n (now - this.#invalidRequests.windowStart);\n console.warn(\n `Temporarily banned from Discord API. Waiting ${Math.ceil(waitTime / 1000)}s`\n );\n await sleep(waitTime);\n this.#resetInvalidRequestCounter();\n }\n\n // Check global rate limit (50 req/s)\n await this.#enforceGlobalRateLimit();\n\n // Check if we're under a global rate limit cooldown\n if (this.#globalReset > now / 1000) {\n const waitTime = (this.#globalReset - now / 1000) * 1000;\n await sleep(waitTime);\n continue;\n }\n\n const request = this.#requestQueue[0];\n if (typeof request === `undefined`) break;\n const bucket = this.#buckets.get(`${request.method}:${request.resource}`);\n\n if (bucket?.remaining === 0) {\n const resetTime = bucket.reset * 1000; // Convert to ms\n if (resetTime > now) {\n await sleep(resetTime - now);\n }\n }\n\n // Execute the request\n this.#requestQueue.shift();\n try {\n const response = await this.#executeRequest(request);\n request.resolve(response);\n } catch (error) {\n if (error instanceof Error) {\n request.reject(error);\n }\n }\n }\n\n this.#processingQueue = false;\n\n // Check if any requests were added while we were processing the last one\n if (this.#requestQueue.length > 0) {\n void this.#processQueue(); // Restart the processor\n }\n };\n\n /**\n * Execute a single request and handle rate limit headers\n */\n #executeRequest = async (\n request: QueuedRequest,\n retryCount = 0\n ): Promise<Response> => {\n const now = Date.now();\n\n // Track this request for global rate limiting\n this.#globalRequestTimestamps.push(now);\n\n const headers: HeadersInit = {};\n\n // Anonymous endpoints (webhook/interaction tokens in the URL) must NOT\n // send an Authorization header — Discord rejects requests that present\n // both forms of auth. Otherwise we use the token captured for this\n // request at enqueue time (see queueRequest), which isolates per-request\n // (e.g. OAuth2 bearer) tokens from concurrent callers.\n if (!request.options?.anonymous) {\n headers.Authorization = request.token ?? this.getSession();\n }\n\n // Audit-log reason. URL-encode so non-ASCII characters survive the\n // single-line HTTP header field.\n if (request.options?.reason) {\n headers[`X-Audit-Log-Reason`] = encodeURIComponent(\n request.options.reason\n );\n }\n\n // FormData bodies (file uploads) must NOT have a manual Content-Type\n // header — fetch sets it to `multipart/form-data; boundary=...`\n // automatically. JSON bodies get application/json.\n if (request.body && !(request.body instanceof FormData)) {\n headers[`Content-Type`] = `application/json`;\n }\n\n const response = await fetch(request.resource.toString(), {\n method: request.method,\n body: request.body,\n headers\n });\n\n // Update rate limit tracking from headers\n this.#updateRateLimits(response);\n\n // Handle 429 Too Many Requests\n if (response.status === 429) {\n if (retryCount >= this.maxRetries) {\n console.error(\n `Max retries (${this.maxRetries}) exceeded for ${request.resource}`\n );\n return response; // Return the 429 response instead of retrying forever\n }\n\n const retryAfter = response.headers.get(`Retry-After`) ?? `1`;\n const isGlobal = response.headers.get(`X-RateLimit-Global`) === `true`;\n const scope = response.headers.get(`X-RateLimit-Scope`);\n\n if (isGlobal || scope === `global`) {\n console.warn(`Hit global rate limit`);\n this.#globalReset = now / 1000 + parseFloat(retryAfter);\n }\n\n // Wait before retrying\n await sleep(parseFloat(retryAfter) * 1000);\n\n // Retry with incremented counter\n return this.#executeRequest(request, retryCount + 1);\n }\n\n // Track invalid requests (401, 403, 429 excluding shared scope)\n if (\n [401, 403].includes(response.status) ||\n (response.status === 429 &&\n response.headers.get(`X-RateLimit-Scope`) !== `shared`)\n ) {\n this.#trackInvalidRequest();\n }\n\n return response;\n };\n\n /**\n * Update rate limit buckets from response headers\n */\n #updateRateLimits = (response: Response): void => {\n const bucket = response.headers.get(`X-RateLimit-Bucket`);\n const limit = response.headers.get(`X-RateLimit-Limit`);\n const remaining = response.headers.get(`X-RateLimit-Remaining`);\n const reset = response.headers.get(`X-RateLimit-Reset`);\n const resetAfter = response.headers.get(`X-RateLimit-Reset-After`);\n\n if (bucket && limit && remaining && reset && resetAfter) {\n this.#buckets.set(bucket, {\n limit: parseInt(limit, 10),\n remaining: parseInt(remaining, 10),\n reset: parseFloat(reset),\n resetAfter: parseFloat(resetAfter)\n });\n }\n };\n\n /**\n * Enforce global rate limit of 50 requests per second\n */\n #enforceGlobalRateLimit = async (): Promise<void> => {\n const now = Date.now();\n\n // Remove timestamps older than 1 second\n this.#globalRequestTimestamps = this.#globalRequestTimestamps.filter(\n (timestamp) => now - timestamp < this.#globalWindow\n );\n\n // If we're at the limit, wait\n if (this.#globalRequestTimestamps.length >= this.#globalLimit) {\n const oldestTimestamp = this.#globalRequestTimestamps[0];\n const waitTime = this.#globalWindow - (now - oldestTimestamp);\n if (waitTime > 0) {\n await sleep(waitTime);\n }\n }\n };\n\n /**\n * Track invalid requests for Cloudflare ban prevention\n */\n #trackInvalidRequest = (): void => {\n const now = Date.now();\n\n // Reset counter if window has passed\n if (now - this.#invalidRequests.windowStart >= this.#invalidRequestWindow) {\n this.#resetInvalidRequestCounter();\n }\n\n this.#invalidRequests.count++;\n\n if (this.#invalidRequests.count >= this.#invalidRequestLimit) {\n console.error(\n `Approaching invalid request limit! Bot may be temporarily banned.`\n );\n }\n };\n\n /**\n * Check if we're temporarily banned\n */\n #isTemporarilyBanned = (): boolean => {\n const now = Date.now();\n if (now - this.#invalidRequests.windowStart < this.#invalidRequestWindow) {\n return this.#invalidRequests.count >= this.#invalidRequestLimit;\n }\n return false;\n };\n\n /**\n * Reset invalid request counter\n */\n #resetInvalidRequestCounter = (): void => {\n this.#invalidRequests = {\n count: 0,\n windowStart: Date.now()\n };\n };\n\n /**\n * Get current queue size (useful for monitoring)\n */\n getQueueSize = (): number => this.#requestQueue.length;\n}\n\nexport const discord = new DiscordSession();\n"],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/requests/addParams.d.ts
|
|
2
|
+
type RequestParams = Partial<Record<string, number[] | string[] | boolean | number | string | null | undefined>>;
|
|
3
|
+
/**
|
|
4
|
+
* Append a params object to a URL.
|
|
5
|
+
*
|
|
6
|
+
* Array values are emitted as repeated query keys (`?id=1&id=2`), which is
|
|
7
|
+
* how Discord's HTTP API documents array query strings. Scalars are
|
|
8
|
+
* stringified once. Keys are converted from camelCase to snake_case to
|
|
9
|
+
* match Discord's convention.
|
|
10
|
+
*
|
|
11
|
+
* @__NO_SIDE_EFFECTS__
|
|
12
|
+
*/
|
|
13
|
+
declare const addParams: (url: URL, params: RequestParams) => URL;
|
|
14
|
+
//#endregion
|
|
15
|
+
export { RequestParams, addParams };
|
|
16
|
+
//# sourceMappingURL=addParams.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addParams.d.mts","names":[],"sources":["../../src/requests/addParams.ts"],"mappings":";KAGY,aAAA,GAAgB,OAAO,CACjC,MAAA;AADF;;;;AACQ;AAgBR;;;;;AAjBA,cAiBa,SAAA,GAAa,GAAA,EAAK,GAAA,EAAK,MAAA,EAAQ,aAAA,KAAgB,GAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { isNonNullable } from "../utils/isNonNullable.mjs";
|
|
2
|
+
import { toSnakeCase } from "../utils/toSnakeCase.mjs";
|
|
3
|
+
//#region src/requests/addParams.ts
|
|
4
|
+
/**
|
|
5
|
+
* Append a params object to a URL.
|
|
6
|
+
*
|
|
7
|
+
* Array values are emitted as repeated query keys (`?id=1&id=2`), which is
|
|
8
|
+
* how Discord's HTTP API documents array query strings. Scalars are
|
|
9
|
+
* stringified once. Keys are converted from camelCase to snake_case to
|
|
10
|
+
* match Discord's convention.
|
|
11
|
+
*
|
|
12
|
+
* @__NO_SIDE_EFFECTS__
|
|
13
|
+
*/
|
|
14
|
+
const addParams = (url, params) => {
|
|
15
|
+
for (const [key, value] of Object.entries(params)) {
|
|
16
|
+
if (!isNonNullable(value)) continue;
|
|
17
|
+
const snake = toSnakeCase(key);
|
|
18
|
+
if (Array.isArray(value)) for (const item of value) url.searchParams.append(snake, String(item));
|
|
19
|
+
else url.searchParams.set(snake, String(value));
|
|
20
|
+
}
|
|
21
|
+
return url;
|
|
22
|
+
};
|
|
23
|
+
//#endregion
|
|
24
|
+
export { addParams };
|
|
25
|
+
|
|
26
|
+
//# sourceMappingURL=addParams.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"addParams.mjs","names":[],"sources":["../../src/requests/addParams.ts"],"sourcesContent":["import { isNonNullable } from \"../utils/isNonNullable.js\";\nimport { toSnakeCase } from \"../utils/toSnakeCase.js\";\n\nexport type RequestParams = Partial<\n Record<\n string,\n number[] | string[] | boolean | number | string | null | undefined\n >\n>;\n\n/**\n * Append a params object to a URL.\n *\n * Array values are emitted as repeated query keys (`?id=1&id=2`), which is\n * how Discord's HTTP API documents array query strings. Scalars are\n * stringified once. Keys are converted from camelCase to snake_case to\n * match Discord's convention.\n *\n * @__NO_SIDE_EFFECTS__\n */\nexport const addParams = (url: URL, params: RequestParams): URL => {\n for (const [key, value] of Object.entries(params)) {\n if (!isNonNullable(value)) continue;\n const snake = toSnakeCase(key);\n if (Array.isArray(value)) {\n for (const item of value) url.searchParams.append(snake, String(item));\n } else {\n url.searchParams.set(snake, String(value));\n }\n }\n\n return url;\n};\n"],"mappings":";;;;;;;;;;;;;AAoBA,MAAa,aAAa,KAAU,WAA+B;CACjE,KAAK,MAAM,CAAC,KAAK,UAAU,OAAO,QAAQ,MAAM,GAAG;EACjD,IAAI,CAAC,cAAc,KAAK,GAAG;EAC3B,MAAM,QAAQ,YAAY,GAAG;EAC7B,IAAI,MAAM,QAAQ,KAAK,GACrB,KAAK,MAAM,QAAQ,OAAO,IAAI,aAAa,OAAO,OAAO,OAAO,IAAI,CAAC;OAErE,IAAI,aAAa,IAAI,OAAO,OAAO,KAAK,CAAC;CAE7C;CAEA,OAAO;AACT"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { RequestParams } from "./addParams.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/requests/buildURL.d.ts
|
|
4
|
+
/** @__NO_SIDE_EFFECTS__ */
|
|
5
|
+
declare const buildURL: (resource: string, params?: RequestParams, base?: string) => URL;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { buildURL };
|
|
8
|
+
//# sourceMappingURL=buildURL.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildURL.d.mts","names":[],"sources":["../../src/requests/buildURL.ts"],"mappings":";;;;cAIa,QAAA,GACX,QAAA,UACA,MAAA,GAAS,aAAA,EACT,IAAA,cACC,GAIA"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { endpoint } from "./DiscordSession.mjs";
|
|
2
|
+
import { addParams } from "./addParams.mjs";
|
|
3
|
+
//#region src/requests/buildURL.ts
|
|
4
|
+
/** @__NO_SIDE_EFFECTS__ */
|
|
5
|
+
const buildURL = (resource, params, base) => addParams(new URL(resource.replace(/^\//, ``), base ?? endpoint), params ?? {});
|
|
6
|
+
//#endregion
|
|
7
|
+
export { buildURL };
|
|
8
|
+
|
|
9
|
+
//# sourceMappingURL=buildURL.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"buildURL.mjs","names":[],"sources":["../../src/requests/buildURL.ts"],"sourcesContent":["import { endpoint } from \"./DiscordSession.js\";\nimport { addParams, type RequestParams } from \"./addParams.js\";\n\n/** @__NO_SIDE_EFFECTS__ */\nexport const buildURL = (\n resource: string,\n params?: RequestParams,\n base?: string\n): URL =>\n addParams(\n new URL(resource.replace(/^\\//, ``), base ?? endpoint),\n params ?? {}\n );\n"],"mappings":";;;;AAIA,MAAa,YACX,UACA,QACA,SAEA,UACE,IAAI,IAAI,SAAS,QAAQ,OAAO,EAAE,GAAG,QAAQ,QAAQ,GACrD,UAAU,CAAC,CACb"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { buildURL } from "./buildURL.mjs";
|
|
2
|
+
|
|
3
|
+
//#region src/requests/getAsset.d.ts
|
|
4
|
+
/** @__NO_SIDE_EFFECTS__ */
|
|
5
|
+
declare const getAsset: (resource: string, params?: Parameters<typeof buildURL>[1]) => string;
|
|
6
|
+
//#endregion
|
|
7
|
+
export { getAsset };
|
|
8
|
+
//# sourceMappingURL=getAsset.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getAsset.d.mts","names":[],"sources":["../../src/requests/getAsset.ts"],"mappings":";;;;cAGa,QAAA,GACX,QAAA,UACA,MAAA,GAAS,UAAU,QAAQ,QAAA"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { buildURL } from "./buildURL.mjs";
|
|
2
|
+
//#region src/requests/getAsset.ts
|
|
3
|
+
/** @__NO_SIDE_EFFECTS__ */
|
|
4
|
+
const getAsset = (resource, params) => buildURL(resource, params, `https://cdn.discordapp.com/`).href;
|
|
5
|
+
//#endregion
|
|
6
|
+
export { getAsset };
|
|
7
|
+
|
|
8
|
+
//# sourceMappingURL=getAsset.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"getAsset.mjs","names":[],"sources":["../../src/requests/getAsset.ts"],"sourcesContent":["import { buildURL } from \"./buildURL.js\";\n\n/** @__NO_SIDE_EFFECTS__ */\nexport const getAsset = (\n resource: string,\n params?: Parameters<typeof buildURL>[1]\n): string => buildURL(resource, params, `https://cdn.discordapp.com/`).href;\n"],"mappings":";;;AAGA,MAAa,YACX,UACA,WACW,SAAS,UAAU,QAAQ,6BAA6B,EAAE"}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { discord, endpoint } from "./DiscordSession.mjs";
|
|
2
|
+
import { buildURL } from "./buildURL.mjs";
|
|
3
|
+
import { getAsset } from "./getAsset.mjs";
|
|
4
|
+
import { Fetcher, FetcherCapabilities, RequestOptionsFor, get, patch, post, put, remove } from "./methods.mjs";
|
|
5
|
+
import { toProcedure } from "./toProcedure.mjs";
|
|
6
|
+
import { QueryFunction, toQuery } from "./toQuery.mjs";
|
|
7
|
+
import { toValidated } from "./toValidated.mjs";
|
|
8
|
+
import { verifyKey } from "./verifyKey.mjs";
|
|
9
|
+
export { Fetcher, FetcherCapabilities, QueryFunction, RequestOptionsFor, buildURL, discord, endpoint, get, getAsset, patch, post, put, remove, toProcedure, toQuery, toValidated, verifyKey };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { discord, endpoint } from "./DiscordSession.mjs";
|
|
2
|
+
import { buildURL } from "./buildURL.mjs";
|
|
3
|
+
import { getAsset } from "./getAsset.mjs";
|
|
4
|
+
import { get, patch, post, put, remove } from "./methods.mjs";
|
|
5
|
+
import { toProcedure } from "./toProcedure.mjs";
|
|
6
|
+
import { toQuery } from "./toQuery.mjs";
|
|
7
|
+
import { toValidated } from "./toValidated.mjs";
|
|
8
|
+
import { verifyKey } from "./verifyKey.mjs";
|
|
9
|
+
export { buildURL, discord, endpoint, get, getAsset, patch, post, put, remove, toProcedure, toQuery, toValidated, verifyKey };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { RequestBody, RequestOptions } from "./request.mjs";
|
|
2
|
+
import { RequestParams } from "./addParams.mjs";
|
|
3
|
+
import { GenericSchema, GenericSchemaAsync, InferOutput } from "valibot";
|
|
4
|
+
|
|
5
|
+
//#region src/requests/methods.d.ts
|
|
6
|
+
/**
|
|
7
|
+
* Capability flags advertised by a {@link Fetcher}. Each flag describes a
|
|
8
|
+
* property of the underlying Discord endpoint — not a per-call preference.
|
|
9
|
+
*
|
|
10
|
+
* The flags drive both the `options` argument the fetcher accepts and the
|
|
11
|
+
* headers/auth the request layer emits at runtime.
|
|
12
|
+
*/
|
|
13
|
+
interface FetcherCapabilities {
|
|
14
|
+
/**
|
|
15
|
+
* `true` if the Discord endpoint MUST be called without an `Authorization`
|
|
16
|
+
* header. Forces callers to acknowledge the unauthenticated path via
|
|
17
|
+
* `{ anonymous: true }` and tells the request layer to skip the session token.
|
|
18
|
+
*/
|
|
19
|
+
readonly anonymous?: boolean;
|
|
20
|
+
/**
|
|
21
|
+
* `true` if the Discord endpoint accepts the `X-Audit-Log-Reason` header.
|
|
22
|
+
* Allows callers to pass `{ reason: "…" }`; the header is URL-encoded and
|
|
23
|
+
* attached by the request layer.
|
|
24
|
+
*/
|
|
25
|
+
readonly auditLogReason?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The shape of the per-call `options` argument a {@link Fetcher} accepts,
|
|
29
|
+
* derived from the endpoint's {@link FetcherCapabilities}.
|
|
30
|
+
*
|
|
31
|
+
* - When `anonymous: true` is declared on the capability, the option is
|
|
32
|
+
* *required* on every call. When omitted, `anonymous` is forbidden.
|
|
33
|
+
* - When `auditLogReason: true` is declared, `reason` is permitted; otherwise
|
|
34
|
+
* it is forbidden.
|
|
35
|
+
*/
|
|
36
|
+
type RequestOptionsFor<C extends FetcherCapabilities> = (C extends {
|
|
37
|
+
anonymous: true;
|
|
38
|
+
} ? {
|
|
39
|
+
anonymous: true;
|
|
40
|
+
} : {
|
|
41
|
+
anonymous?: never;
|
|
42
|
+
}) & (C extends {
|
|
43
|
+
auditLogReason: true;
|
|
44
|
+
} ? {
|
|
45
|
+
reason?: string;
|
|
46
|
+
} : {
|
|
47
|
+
reason?: never;
|
|
48
|
+
});
|
|
49
|
+
/**
|
|
50
|
+
* Decide whether the per-call `options` argument is required or optional
|
|
51
|
+
* based on whether any capability forces a value to be passed.
|
|
52
|
+
*/
|
|
53
|
+
type RequiresOptions<C extends FetcherCapabilities> = C extends {
|
|
54
|
+
anonymous: true;
|
|
55
|
+
} ? true : false;
|
|
56
|
+
type Fetcher< /** A schema to validate the input arguments of a fetch call */S extends GenericSchema | GenericSchemaAsync | null = null, /** The return value expected from the fetch call */R = void, /** Endpoint capabilities; shapes the `options` argument */C extends FetcherCapabilities = {}> = S extends null ? RequiresOptions<C> extends true ? (options: RequestOptionsFor<C>) => Promise<R> : (options?: RequestOptionsFor<C>) => Promise<R> : RequiresOptions<C> extends true ? (config: InferOutput<NonNullable<S>>, options: RequestOptionsFor<C>) => Promise<R> : (config: InferOutput<NonNullable<S>>, options?: RequestOptionsFor<C>) => Promise<R>;
|
|
57
|
+
declare const get: <T>(url: string, params?: RequestParams, options?: RequestOptions) => Promise<T>;
|
|
58
|
+
declare const post: <T>(url: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
59
|
+
declare const put: <T>(url: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
60
|
+
declare const patch: <T>(url: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
61
|
+
declare const remove: <T = void>(url: string, options?: RequestOptions) => Promise<T>;
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Fetcher, FetcherCapabilities, RequestOptionsFor, get, patch, post, put, remove };
|
|
64
|
+
//# sourceMappingURL=methods.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"methods.d.mts","names":[],"sources":["../../src/requests/methods.ts"],"mappings":";;;;;;;AAaA;;;;AAYyB;UAZR,mBAAA;EAwBY;;;;;EAAA,SAlBlB,SAAA;EAwBL;;;;;EAAA,SAlBK,cAAc;AAAA;;;;;;AAoBT;AAAY;;;KARhB,iBAAA,WAA4B,mBAAA,KAErC,CAAA;EAAY,SAAA;AAAA;EACP,SAAA;AAAA;EACA,SAAA;AAAA,MAEH,CAAA;EAAY,cAAA;AAAA;EACP,MAAA;AAAA;EACA,MAAA;AAAA;;;;;KAML,eAAA,WAA0B,mBAAA,IAAuB,CAAC;EACrD,SAAA;AAAA;AAAA,KAKU,OAAA,2EAEA,aAAA,GAAgB,kBAAA,mJAKhB,mBAAA,SACR,CAAA,gBACA,eAAA,CAAgB,CAAA,kBACb,OAAA,EAAS,iBAAA,CAAkB,CAAA,MAAO,OAAA,CAAQ,CAAA,KAC1C,OAAA,GAAU,iBAAA,CAAkB,CAAA,MAAO,OAAA,CAAQ,CAAA,IAC9C,eAAA,CAAgB,CAAA,kBAEZ,MAAA,EAAQ,WAAA,CAAY,WAAA,CAAY,CAAA,IAChC,OAAA,EAAS,iBAAA,CAAkB,CAAA,MACxB,OAAA,CAAQ,CAAA,KAEX,MAAA,EAAQ,WAAA,CAAY,WAAA,CAAY,CAAA,IAChC,OAAA,GAAU,iBAAA,CAAkB,CAAA,MACzB,OAAA,CAAQ,CAAA;AAAA,cAEN,GAAA,MACX,GAAA,UACA,MAAA,GAAS,aAAA,EACT,OAAA,GAAU,cAAA,KACT,OAAA,CAAQ,CAAA;AAAA,cAEE,IAAA,MACX,GAAA,UACA,IAAA,GAAO,WAAA,EACP,OAAA,GAAU,cAAA,KACT,OAAA,CAAQ,CAAA;AAAA,cAEE,GAAA,MACX,GAAA,UACA,IAAA,GAAO,WAAA,EACP,OAAA,GAAU,cAAA,KACT,OAAA,CAAQ,CAAA;AAAA,cAEE,KAAA,MACX,GAAA,UACA,IAAA,GAAO,WAAA,EACP,OAAA,GAAU,cAAA,KACT,OAAA,CAAQ,CAAA;AAAA,cAEE,MAAA,aACX,GAAA,UACA,OAAA,GAAU,cAAA,KACT,OAAA,CAAQ,CAAA"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { buildURL } from "./buildURL.mjs";
|
|
2
|
+
import { request } from "./request.mjs";
|
|
3
|
+
//#region src/requests/methods.ts
|
|
4
|
+
const get = async (url, params, options) => request(buildURL(url, params), `GET`, void 0, options);
|
|
5
|
+
const post = async (url, body, options) => request(buildURL(url), `POST`, body, options);
|
|
6
|
+
const put = async (url, body, options) => request(buildURL(url), `PUT`, body, options);
|
|
7
|
+
const patch = async (url, body, options) => request(buildURL(url), `PATCH`, body, options);
|
|
8
|
+
const remove = async (url, options) => request(buildURL(url), `DELETE`, void 0, options);
|
|
9
|
+
//#endregion
|
|
10
|
+
export { get, patch, post, put, remove };
|
|
11
|
+
|
|
12
|
+
//# sourceMappingURL=methods.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"methods.mjs","names":[],"sources":["../../src/requests/methods.ts"],"sourcesContent":["import type { GenericSchema, GenericSchemaAsync, InferOutput } from \"valibot\";\nimport type { RequestParams } from \"./addParams.js\";\nimport { buildURL } from \"./buildURL.js\";\nimport type { RequestBody, RequestOptions } from \"./request.js\";\nimport { request } from \"./request.js\";\n\n/**\n * Capability flags advertised by a {@link Fetcher}. Each flag describes a\n * property of the underlying Discord endpoint — not a per-call preference.\n *\n * The flags drive both the `options` argument the fetcher accepts and the\n * headers/auth the request layer emits at runtime.\n */\nexport interface FetcherCapabilities {\n /**\n * `true` if the Discord endpoint MUST be called without an `Authorization`\n * header. Forces callers to acknowledge the unauthenticated path via\n * `{ anonymous: true }` and tells the request layer to skip the session token.\n */\n readonly anonymous?: boolean;\n /**\n * `true` if the Discord endpoint accepts the `X-Audit-Log-Reason` header.\n * Allows callers to pass `{ reason: \"…\" }`; the header is URL-encoded and\n * attached by the request layer.\n */\n readonly auditLogReason?: boolean;\n}\n\n/**\n * The shape of the per-call `options` argument a {@link Fetcher} accepts,\n * derived from the endpoint's {@link FetcherCapabilities}.\n *\n * - When `anonymous: true` is declared on the capability, the option is\n * *required* on every call. When omitted, `anonymous` is forbidden.\n * - When `auditLogReason: true` is declared, `reason` is permitted; otherwise\n * it is forbidden.\n */\nexport type RequestOptionsFor<C extends FetcherCapabilities> =\n // `anonymous` slot\n (C extends { anonymous: true }\n ? { anonymous: true }\n : { anonymous?: never }) &\n // `reason` slot\n (C extends { auditLogReason: true }\n ? { reason?: string }\n : { reason?: never });\n\n/**\n * Decide whether the per-call `options` argument is required or optional\n * based on whether any capability forces a value to be passed.\n */\ntype RequiresOptions<C extends FetcherCapabilities> = C extends {\n anonymous: true;\n}\n ? true\n : false;\n\nexport type Fetcher<\n /** A schema to validate the input arguments of a fetch call */\n S extends GenericSchema | GenericSchemaAsync | null = null,\n /** The return value expected from the fetch call */\n R = void,\n /** Endpoint capabilities; shapes the `options` argument */\n // oxlint-disable-next-line typescript/no-empty-object-type\n C extends FetcherCapabilities = {}\n> = S extends null\n ? RequiresOptions<C> extends true\n ? (options: RequestOptionsFor<C>) => Promise<R>\n : (options?: RequestOptionsFor<C>) => Promise<R>\n : RequiresOptions<C> extends true\n ? (\n config: InferOutput<NonNullable<S>>,\n options: RequestOptionsFor<C>\n ) => Promise<R>\n : (\n config: InferOutput<NonNullable<S>>,\n options?: RequestOptionsFor<C>\n ) => Promise<R>;\n\nexport const get = async <T>(\n url: string,\n params?: RequestParams,\n options?: RequestOptions\n): Promise<T> => request(buildURL(url, params), `GET`, undefined, options);\n\nexport const post = async <T>(\n url: string,\n body?: RequestBody,\n options?: RequestOptions\n): Promise<T> => request(buildURL(url), `POST`, body, options);\n\nexport const put = async <T>(\n url: string,\n body?: RequestBody,\n options?: RequestOptions\n): Promise<T> => request(buildURL(url), `PUT`, body, options);\n\nexport const patch = async <T>(\n url: string,\n body?: RequestBody,\n options?: RequestOptions\n): Promise<T> => request(buildURL(url), `PATCH`, body, options);\n\nexport const remove = async <T = void>(\n url: string,\n options?: RequestOptions\n): Promise<T> => request(buildURL(url), `DELETE`, undefined, options);\n"],"mappings":";;;AA+EA,MAAa,MAAM,OACjB,KACA,QACA,YACe,QAAQ,SAAS,KAAK,MAAM,GAAG,OAAO,KAAA,GAAW,OAAO;AAEzE,MAAa,OAAO,OAClB,KACA,MACA,YACe,QAAQ,SAAS,GAAG,GAAG,QAAQ,MAAM,OAAO;AAE7D,MAAa,MAAM,OACjB,KACA,MACA,YACe,QAAQ,SAAS,GAAG,GAAG,OAAO,MAAM,OAAO;AAE5D,MAAa,QAAQ,OACnB,KACA,MACA,YACe,QAAQ,SAAS,GAAG,GAAG,SAAS,MAAM,OAAO;AAE9D,MAAa,SAAS,OACpB,KACA,YACe,QAAQ,SAAS,GAAG,GAAG,UAAU,KAAA,GAAW,OAAO"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
//#region src/requests/request.d.ts
|
|
2
|
+
type RequestBody = object | null | undefined;
|
|
3
|
+
/**
|
|
4
|
+
* Per-call request options forwarded by the typed method helpers
|
|
5
|
+
* (`get`/`post`/`put`/`patch`/`remove`) into the request layer.
|
|
6
|
+
*
|
|
7
|
+
* Endpoints surface these via the `Fetcher` type so the call site is
|
|
8
|
+
* type-checked against the endpoint's declared capabilities.
|
|
9
|
+
*/
|
|
10
|
+
interface RequestOptions {
|
|
11
|
+
/**
|
|
12
|
+
* Skip the `Authorization` header for this request. Used by webhook
|
|
13
|
+
* and interaction endpoints whose path tokens act as authorization.
|
|
14
|
+
* When set, `discord.getSession()` is NOT consulted, so the session
|
|
15
|
+
* is allowed to be unset.
|
|
16
|
+
*/
|
|
17
|
+
anonymous?: boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Audit-log reason. Forwarded to Discord as the `X-Audit-Log-Reason`
|
|
20
|
+
* header. The value is URL-encoded by the request layer so non-ASCII
|
|
21
|
+
* characters survive transit.
|
|
22
|
+
*/
|
|
23
|
+
reason?: string;
|
|
24
|
+
}
|
|
25
|
+
declare const request: <T>(resource: URL, method?: string, body?: RequestBody, options?: RequestOptions) => Promise<T>;
|
|
26
|
+
//#endregion
|
|
27
|
+
export { RequestBody, RequestOptions, request };
|
|
28
|
+
//# sourceMappingURL=request.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.d.mts","names":[],"sources":["../../src/requests/request.ts"],"mappings":";KAQY,WAAA;AAAZ;;;;AAAuB;AASvB;;AATA,UASiB,cAAA;EAOf;AAMM;AAGR;;;;EATE,SAAA;EAaU;;;;;EAPV,MAAM;AAAA;AAAA,cAGK,OAAA,MACX,QAAA,EAAU,GAAA,EACV,MAAA,WACA,IAAA,GAAO,WAAA,EACP,OAAA,GAAU,cAAA,KACT,OAAA,CAAQ,CAAA"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { discord } from "./DiscordSession.mjs";
|
|
2
|
+
import { toCamelKeys } from "../utils/toCamelKeys.mjs";
|
|
3
|
+
import { toSnakeKeys } from "../utils/toSnakeKeys.mjs";
|
|
4
|
+
import { shouldSerializeAsMultipart, toMultipartBody } from "../validations/fileUpload.mjs";
|
|
5
|
+
//#region src/requests/request.ts
|
|
6
|
+
const request = async (resource, method = `GET`, body, options) => {
|
|
7
|
+
if (!options?.anonymous && !discord.hasAuth) discord.getSession();
|
|
8
|
+
/**
|
|
9
|
+
* Serialize the body. The `multipart()` schema wrapper stamps a
|
|
10
|
+
* sentinel on the parsed body when {@link FileUpload}s are present,
|
|
11
|
+
* which switches serialization from JSON to `multipart/form-data`.
|
|
12
|
+
*/
|
|
13
|
+
const serializeBody = () => {
|
|
14
|
+
if (!body) return body;
|
|
15
|
+
try {
|
|
16
|
+
if (shouldSerializeAsMultipart(body)) return toMultipartBody(body, toSnakeKeys);
|
|
17
|
+
return JSON.stringify(toSnakeKeys(body));
|
|
18
|
+
} catch (cause) {
|
|
19
|
+
console.error(`Received malformed request body:\n\n`, { body });
|
|
20
|
+
throw new Error(`Failed to serialize request body!`, { cause });
|
|
21
|
+
}
|
|
22
|
+
};
|
|
23
|
+
const res = await discord.queueRequest(resource, method, serializeBody(), options);
|
|
24
|
+
if (!res.ok) throw new Error(`Request to resource '${resource.toString()}' failed:\n\n${res.statusText}`);
|
|
25
|
+
if (res.status === 204) return;
|
|
26
|
+
return toCamelKeys(await res.json());
|
|
27
|
+
};
|
|
28
|
+
//#endregion
|
|
29
|
+
export { request };
|
|
30
|
+
|
|
31
|
+
//# sourceMappingURL=request.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"request.mjs","names":[],"sources":["../../src/requests/request.ts"],"sourcesContent":["import { discord } from \"./DiscordSession.js\";\nimport { toCamelKeys } from \"../utils/toCamelKeys.js\";\nimport { toSnakeKeys } from \"../utils/toSnakeKeys.js\";\nimport {\n shouldSerializeAsMultipart,\n toMultipartBody\n} from \"../validations/fileUpload.js\";\n\nexport type RequestBody = object | null | undefined;\n\n/**\n * Per-call request options forwarded by the typed method helpers\n * (`get`/`post`/`put`/`patch`/`remove`) into the request layer.\n *\n * Endpoints surface these via the `Fetcher` type so the call site is\n * type-checked against the endpoint's declared capabilities.\n */\nexport interface RequestOptions {\n /**\n * Skip the `Authorization` header for this request. Used by webhook\n * and interaction endpoints whose path tokens act as authorization.\n * When set, `discord.getSession()` is NOT consulted, so the session\n * is allowed to be unset.\n */\n anonymous?: boolean;\n /**\n * Audit-log reason. Forwarded to Discord as the `X-Audit-Log-Reason`\n * header. The value is URL-encoded by the request layer so non-ASCII\n * characters survive transit.\n */\n reason?: string;\n}\n\nexport const request = async <T>(\n resource: URL,\n method = `GET`,\n body?: RequestBody,\n options?: RequestOptions\n): Promise<T> => {\n // Non-anonymous requests need *some* auth: either an active per-request\n // token or a session token. Anonymous endpoints (webhook/interaction tokens\n // in the URL) need neither. Fail early with a clear message if none is set.\n if (!options?.anonymous && !discord.hasAuth) {\n discord.getSession(); // throws the canonical \"Auth Token must be set\" error\n }\n\n /**\n * Serialize the body. The `multipart()` schema wrapper stamps a\n * sentinel on the parsed body when {@link FileUpload}s are present,\n * which switches serialization from JSON to `multipart/form-data`.\n */\n const serializeBody = (): string | FormData | null | undefined => {\n if (!body) return body;\n try {\n if (shouldSerializeAsMultipart(body)) {\n return toMultipartBody(body, toSnakeKeys);\n }\n return JSON.stringify(toSnakeKeys(body));\n } catch (cause) {\n console.error(`Received malformed request body:\\n\\n`, { body });\n throw new Error(`Failed to serialize request body!`, { cause });\n }\n };\n\n // Queue the request through the rate limiter\n const res = await discord.queueRequest(\n resource,\n method,\n serializeBody(),\n options\n );\n\n if (!res.ok) {\n throw new Error(\n `Request to resource '${resource.toString()}' failed:\\n\\n${res.statusText}`\n );\n }\n\n // Return `void` on `204 No Content` responses\n if (res.status === 204) {\n // @ts-expect-error\n return;\n }\n\n return toCamelKeys(await res.json());\n};\n"],"mappings":";;;;;AAiCA,MAAa,UAAU,OACrB,UACA,SAAS,OACT,MACA,YACe;CAIf,IAAI,CAAC,SAAS,aAAa,CAAC,QAAQ,SAClC,QAAQ,WAAW;;;;;;CAQrB,MAAM,sBAA4D;EAChE,IAAI,CAAC,MAAM,OAAO;EAClB,IAAI;GACF,IAAI,2BAA2B,IAAI,GACjC,OAAO,gBAAgB,MAAM,WAAW;GAE1C,OAAO,KAAK,UAAU,YAAY,IAAI,CAAC;EACzC,SAAS,OAAO;GACd,QAAQ,MAAM,wCAAwC,EAAE,KAAK,CAAC;GAC9D,MAAM,IAAI,MAAM,qCAAqC,EAAE,MAAM,CAAC;EAChE;CACF;CAGA,MAAM,MAAM,MAAM,QAAQ,aACxB,UACA,QACA,cAAc,GACd,OACF;CAEA,IAAI,CAAC,IAAI,IACP,MAAM,IAAI,MACR,wBAAwB,SAAS,SAAS,EAAE,eAAe,IAAI,YACjE;CAIF,IAAI,IAAI,WAAW,KAEjB;CAGF,OAAO,YAAY,MAAM,IAAI,KAAK,CAAC;AACrC"}
|