seyfert 4.4.1 → 4.6.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.
Files changed (180) hide show
  1. package/README.md +7 -1
  2. package/lib/api/Routes/guilds.d.ts +12 -1
  3. package/lib/api/Routes/index.d.ts +3 -1
  4. package/lib/api/Routes/invites.d.ts +1 -1
  5. package/lib/api/Routes/oauth2.d.ts +11 -0
  6. package/lib/api/Routes/oauth2.js +2 -0
  7. package/lib/api/Routes/skus.d.ts +11 -4
  8. package/lib/api/Routes/users.d.ts +2 -1
  9. package/lib/api/Routes/voice.d.ts +4 -0
  10. package/lib/api/Routes/webhooks.d.ts +1 -1
  11. package/lib/api/api.d.ts +15 -5
  12. package/lib/api/api.js +243 -114
  13. package/lib/api/bucket.js +13 -7
  14. package/lib/api/shared.d.ts +3 -1
  15. package/lib/api/utils/constants.d.ts +1 -1
  16. package/lib/api/utils/constants.js +1 -1
  17. package/lib/builders/Attachment.d.ts +6 -1
  18. package/lib/builders/Attachment.js +13 -9
  19. package/lib/builders/CheckboxGroup.js +1 -1
  20. package/lib/builders/FileUpload.d.ts +8 -1
  21. package/lib/builders/FileUpload.js +9 -0
  22. package/lib/builders/RadioGroup.js +1 -1
  23. package/lib/client/base.d.ts +3 -3
  24. package/lib/client/client.d.ts +1 -1
  25. package/lib/client/client.js +5 -4
  26. package/lib/client/collectors.d.ts +10 -8
  27. package/lib/client/transformers.d.ts +5 -0
  28. package/lib/client/workerclient.d.ts +8 -1
  29. package/lib/client/workerclient.js +34 -14
  30. package/lib/commands/applications/chat.d.ts +6 -4
  31. package/lib/commands/applications/options.d.ts +6 -4
  32. package/lib/commands/applications/shared.d.ts +2 -2
  33. package/lib/commands/handle.d.ts +7 -3
  34. package/lib/commands/handle.js +36 -11
  35. package/lib/commands/handler.js +11 -0
  36. package/lib/commands/optionresolver.d.ts +6 -7
  37. package/lib/common/it/error.d.ts +13 -0
  38. package/lib/common/it/error.js +15 -1
  39. package/lib/common/it/logger.js +12 -9
  40. package/lib/common/shorters/application.d.ts +1 -1
  41. package/lib/common/shorters/application.js +1 -1
  42. package/lib/common/shorters/bans.js +1 -1
  43. package/lib/common/shorters/guilds.js +5 -1
  44. package/lib/common/shorters/interaction.js +7 -4
  45. package/lib/common/shorters/invites.d.ts +5 -0
  46. package/lib/common/shorters/members.js +2 -1
  47. package/lib/common/shorters/messages.js +2 -2
  48. package/lib/common/shorters/threads.js +1 -1
  49. package/lib/common/shorters/webhook.js +3 -3
  50. package/lib/common/types/util.d.ts +7 -0
  51. package/lib/components/componentcommand.js +2 -2
  52. package/lib/components/customId.d.ts +3 -0
  53. package/lib/components/customId.js +11 -0
  54. package/lib/components/handler.js +2 -3
  55. package/lib/components/modalcommand.js +2 -2
  56. package/lib/events/hooks/invite.d.ts +1 -0
  57. package/lib/events/hooks/presence.d.ts +1 -0
  58. package/lib/events/hooks/thread.d.ts +1 -0
  59. package/lib/index.d.ts +1 -1
  60. package/lib/index.js +2 -1
  61. package/lib/structures/AnonymousGuild.d.ts +1 -1
  62. package/lib/structures/AnonymousGuild.js +1 -1
  63. package/lib/structures/Guild.d.ts +3 -0
  64. package/lib/structures/GuildMember.d.ts +2 -2
  65. package/lib/structures/GuildMember.js +22 -4
  66. package/lib/structures/GuildPreview.d.ts +1 -1
  67. package/lib/structures/GuildPreview.js +1 -1
  68. package/lib/structures/GuildRole.d.ts +13 -0
  69. package/lib/structures/GuildRole.js +21 -0
  70. package/lib/structures/GuildTemplate.js +1 -1
  71. package/lib/structures/Interaction.d.ts +7 -7
  72. package/lib/structures/Interaction.js +35 -21
  73. package/lib/structures/Message.d.ts +2 -1
  74. package/lib/structures/channels.d.ts +29 -16
  75. package/lib/structures/channels.js +59 -19
  76. package/lib/structures/extra/BaseGuild.d.ts +4 -4
  77. package/lib/structures/extra/BaseGuild.js +4 -4
  78. package/lib/types/gateway.d.ts +243 -239
  79. package/lib/types/index.d.ts +2 -2
  80. package/lib/types/payloads/_interactions/_applicationCommands/_chatInput/attachment.d.ts +5 -2
  81. package/lib/types/payloads/_interactions/_applicationCommands/_chatInput/shared.d.ts +2 -2
  82. package/lib/types/payloads/_interactions/_applicationCommands/_chatInput/shared.js +1 -1
  83. package/lib/types/payloads/_interactions/_applicationCommands/chatInput.d.ts +7 -7
  84. package/lib/types/payloads/_interactions/_applicationCommands/contextMenu.d.ts +13 -13
  85. package/lib/types/payloads/_interactions/_applicationCommands/permissions.d.ts +4 -4
  86. package/lib/types/payloads/_interactions/_applicationCommands/permissions.js +2 -2
  87. package/lib/types/payloads/_interactions/applicationCommands.d.ts +10 -10
  88. package/lib/types/payloads/_interactions/applicationCommands.js +4 -4
  89. package/lib/types/payloads/_interactions/autocomplete.d.ts +2 -2
  90. package/lib/types/payloads/_interactions/base.d.ts +13 -7
  91. package/lib/types/payloads/_interactions/modalSubmit.d.ts +4 -4
  92. package/lib/types/payloads/_interactions/responses.d.ts +10 -10
  93. package/lib/types/payloads/_interactions/responses.js +2 -2
  94. package/lib/types/payloads/application.d.ts +21 -15
  95. package/lib/types/payloads/application.js +7 -7
  96. package/lib/types/payloads/auditLog.d.ts +22 -17
  97. package/lib/types/payloads/auditLog.js +7 -2
  98. package/lib/types/payloads/autoModeration.d.ts +11 -11
  99. package/lib/types/payloads/autoModeration.js +5 -5
  100. package/lib/types/payloads/channel.d.ts +87 -78
  101. package/lib/types/payloads/channel.js +16 -11
  102. package/lib/types/payloads/components.d.ts +41 -31
  103. package/lib/types/payloads/components.js +9 -5
  104. package/lib/types/payloads/emoji.d.ts +2 -2
  105. package/lib/types/payloads/emoji.js +1 -1
  106. package/lib/types/payloads/gateway.d.ts +33 -29
  107. package/lib/types/payloads/gateway.js +5 -5
  108. package/lib/types/payloads/guild.d.ts +68 -68
  109. package/lib/types/payloads/guild.js +14 -14
  110. package/lib/types/payloads/guildScheduledEvent.d.ts +11 -11
  111. package/lib/types/payloads/guildScheduledEvent.js +3 -3
  112. package/lib/types/payloads/index.d.ts +2 -2
  113. package/lib/types/payloads/interactions.d.ts +3 -3
  114. package/lib/types/payloads/invite.d.ts +11 -11
  115. package/lib/types/payloads/invite.js +3 -3
  116. package/lib/types/payloads/monetization.d.ts +4 -4
  117. package/lib/types/payloads/monetization.js +2 -2
  118. package/lib/types/payloads/oauth2.d.ts +21 -17
  119. package/lib/types/payloads/oauth2.js +21 -17
  120. package/lib/types/payloads/permissions.d.ts +5 -5
  121. package/lib/types/payloads/permissions.js +2 -2
  122. package/lib/types/payloads/poll.d.ts +7 -7
  123. package/lib/types/payloads/poll.js +2 -2
  124. package/lib/types/payloads/soundboard.d.ts +2 -2
  125. package/lib/types/payloads/soundboard.js +1 -1
  126. package/lib/types/payloads/stageInstance.d.ts +5 -5
  127. package/lib/types/payloads/stageInstance.js +1 -1
  128. package/lib/types/payloads/sticker.d.ts +8 -8
  129. package/lib/types/payloads/sticker.js +3 -3
  130. package/lib/types/payloads/teams.d.ts +8 -8
  131. package/lib/types/payloads/teams.js +3 -3
  132. package/lib/types/payloads/template.d.ts +3 -3
  133. package/lib/types/payloads/template.js +1 -1
  134. package/lib/types/payloads/user.d.ts +24 -22
  135. package/lib/types/payloads/user.js +7 -5
  136. package/lib/types/payloads/voice.d.ts +4 -4
  137. package/lib/types/payloads/voice.js +1 -1
  138. package/lib/types/payloads/webhokEvents.d.ts +5 -5
  139. package/lib/types/payloads/webhokEvents.js +2 -2
  140. package/lib/types/payloads/webhook.d.ts +4 -4
  141. package/lib/types/payloads/webhook.js +1 -1
  142. package/lib/types/rest/application.d.ts +14 -14
  143. package/lib/types/rest/auditLog.d.ts +1 -1
  144. package/lib/types/rest/autoModeration.d.ts +7 -7
  145. package/lib/types/rest/channel.d.ts +79 -74
  146. package/lib/types/rest/channel.js +1 -1
  147. package/lib/types/rest/emoji.d.ts +8 -8
  148. package/lib/types/rest/gateway.d.ts +2 -2
  149. package/lib/types/rest/guild.d.ts +98 -98
  150. package/lib/types/rest/guild.js +5 -5
  151. package/lib/types/rest/guildScheduledEvent.d.ts +11 -11
  152. package/lib/types/rest/index.d.ts +34 -3
  153. package/lib/types/rest/index.js +33 -2
  154. package/lib/types/rest/interactions.d.ts +48 -48
  155. package/lib/types/rest/invite.d.ts +5 -5
  156. package/lib/types/rest/monetization.d.ts +9 -9
  157. package/lib/types/rest/monetization.js +1 -1
  158. package/lib/types/rest/oauth2.d.ts +22 -18
  159. package/lib/types/rest/poll.d.ts +4 -4
  160. package/lib/types/rest/soundboard.d.ts +8 -8
  161. package/lib/types/rest/stageInstance.d.ts +6 -6
  162. package/lib/types/rest/sticker.d.ts +10 -10
  163. package/lib/types/rest/template.d.ts +11 -11
  164. package/lib/types/rest/user.d.ts +18 -14
  165. package/lib/types/rest/voice.d.ts +5 -5
  166. package/lib/types/rest/webhook.d.ts +38 -38
  167. package/lib/types/utils/index.d.ts +19 -19
  168. package/lib/types/utils/index.js +19 -19
  169. package/lib/websocket/SharedTypes.d.ts +2 -2
  170. package/lib/websocket/constants/index.js +2 -2
  171. package/lib/websocket/discord/heartbeater.d.ts +1 -0
  172. package/lib/websocket/discord/heartbeater.js +8 -0
  173. package/lib/websocket/discord/shard.d.ts +19 -10
  174. package/lib/websocket/discord/shard.js +255 -122
  175. package/lib/websocket/discord/sharder.d.ts +2 -2
  176. package/lib/websocket/discord/sharder.js +2 -2
  177. package/lib/websocket/discord/shared.d.ts +7 -2
  178. package/lib/websocket/discord/workermanager.d.ts +3 -1
  179. package/lib/websocket/discord/workermanager.js +31 -24
  180. package/package.json +3 -3
package/lib/api/api.js CHANGED
@@ -8,11 +8,32 @@ const bucket_1 = require("./bucket");
8
8
  const Router_1 = require("./Router");
9
9
  const shared_1 = require("./shared");
10
10
  const utils_2 = require("./utils/utils");
11
+ // JavaScript timers use a signed 32-bit millisecond delay.
12
+ const MAX_TIMER_DELAY_MS = 2_147_483_647;
13
+ const MAX_BUCKET_RESET_DELAY_MS = MAX_TIMER_DELAY_MS - 1;
14
+ const MAX_SMART_BUCKET_RESET_AFTER_MS = Math.floor(MAX_TIMER_DELAY_MS / 1.5);
15
+ function parseRateLimitCount(raw, minimum) {
16
+ if (raw === null)
17
+ return;
18
+ if (!/^\d+$/.test(raw))
19
+ return null;
20
+ const value = Number(raw);
21
+ return Number.isSafeInteger(value) && value >= minimum ? value : null;
22
+ }
23
+ function parseRateLimitDelay(value) {
24
+ if (value === null || (typeof value === 'string' && value.trim() === ''))
25
+ return;
26
+ const milliseconds = Number(value) * 1000;
27
+ return Number.isFinite(milliseconds) && milliseconds >= 0 ? milliseconds : undefined;
28
+ }
11
29
  class ApiHandler {
12
30
  options;
13
31
  globalBlock = false;
14
32
  ratelimits = new Map();
15
33
  readyQueue = [];
34
+ bucketAliases = new Map();
35
+ globalBlocks = new Map();
36
+ globalTimers = new Map();
16
37
  cdn = Router_1.CDNRouter.createProxy();
17
38
  workerPromises;
18
39
  onRatelimit;
@@ -70,6 +91,14 @@ class ApiHandler {
70
91
  return (this._proxy_ ??= new Router_1.Router(this).createProxy());
71
92
  }
72
93
  globalUnblock() {
94
+ for (const timer of this.globalTimers.values())
95
+ clearTimeout(timer);
96
+ this.globalTimers.clear();
97
+ for (const queue of this.globalBlocks.values()) {
98
+ for (const callback of queue)
99
+ callback();
100
+ }
101
+ this.globalBlocks.clear();
73
102
  this.globalBlock = false;
74
103
  let cb;
75
104
  while ((cb = this.readyQueue.shift())) {
@@ -107,7 +136,11 @@ class ApiHandler {
107
136
  this.debugger?.warn('onFailRequest callback error', callbackError);
108
137
  }
109
138
  }
110
- async request(method, url, { auth = true, ...request } = {}) {
139
+ async request(method, url, request = {}) {
140
+ const requestOptions = { ...request };
141
+ const { auth = true } = requestOptions;
142
+ let attempts = requestOptions._50xRetries ?? 0;
143
+ delete requestOptions._50xRetries;
111
144
  const originTrace = {};
112
145
  Error.captureStackTrace(originTrace, this.request);
113
146
  if (this.options.workerProxy) {
@@ -118,11 +151,12 @@ class ApiHandler {
118
151
  type: 'WORKER_API_REQUEST',
119
152
  workerId: this.workerData.workerId,
120
153
  nonce,
121
- requestOptions: { auth, ...request },
154
+ requestOptions: { auth, ...requestOptions },
122
155
  });
123
156
  }
124
- const route = request.route || this.routefy(url, method);
125
- let attempts = 0;
157
+ const route = requestOptions.route || this.routefy(url, method);
158
+ const provisionalRoute = requestOptions.route || `${method}:${route}`;
159
+ let bucketRoute = this.bucketAliases.get(provisionalRoute) ?? provisionalRoute;
126
160
  const callback = async (next, resolve, reject) => {
127
161
  const headers = {
128
162
  'User-Agent': this.options.userAgent,
@@ -130,7 +164,7 @@ class ApiHandler {
130
164
  const { data, finalUrl } = this.parseRequest({
131
165
  url,
132
166
  headers,
133
- request: { ...request, auth },
167
+ request: { ...requestOptions, auth },
134
168
  });
135
169
  let response;
136
170
  try {
@@ -152,77 +186,105 @@ class ApiHandler {
152
186
  }
153
187
  const now = Date.now();
154
188
  const headerNow = Date.parse(response.headers.get('date') ?? '');
155
- this.setRatelimitsBucket(route, response);
156
- this.setResetBucket(route, response, now, headerNow);
157
- let result = await response.text();
189
+ bucketRoute = this.learnBucket(provisionalRoute, bucketRoute, url, response, requestOptions.route !== undefined);
190
+ this.setRatelimitsBucket(bucketRoute, response);
191
+ this.setResetBucket(bucketRoute, response, now, headerNow, route);
192
+ this.ratelimits.get(bucketRoute).process();
193
+ const needsObserverBody = (response.status < 300 && this.onSuccessRequest !== undefined) ||
194
+ (response.status === 429 && this.onRatelimit !== undefined);
195
+ const observerResponse = needsObserverBody ? response.clone() : response;
196
+ let result;
197
+ try {
198
+ result = await decodeResponse(response);
199
+ }
200
+ catch (error) {
201
+ await this.notifyFailRequest(method, finalUrl, error, response.status);
202
+ next();
203
+ reject(error);
204
+ return;
205
+ }
158
206
  if (response.status >= 300) {
207
+ const errorResult = normalizeErrorResult(result);
159
208
  if (response.status === 429) {
160
- const result429 = await this.handle429(route, method, url, request, response, result, next, reject, now);
209
+ const result429 = await this.handle429(bucketRoute, method, url, { ...requestOptions, auth }, observerResponse, typeof errorResult === 'string' ? errorResult : JSON.stringify(errorResult), next, reject, now);
161
210
  if (result429 !== false)
162
211
  return resolve(result429);
163
- await this.notifyFailRequest(method, finalUrl, result, response.status);
164
- return this.clearResetInterval(route);
212
+ await this.notifyFailRequest(method, finalUrl, errorResult, response.status);
213
+ return this.clearResetInterval(bucketRoute);
165
214
  }
166
215
  if ([502, 503].includes(response.status) && ++attempts < 4) {
167
- this.clearResetInterval(route);
168
- return this.handle50X(method, url, request, next);
216
+ this.clearResetInterval(bucketRoute);
217
+ return this.handle50X(method, url, requestOptions, attempts, next, resolve, reject);
169
218
  }
170
- this.clearResetInterval(route);
219
+ this.clearResetInterval(bucketRoute);
171
220
  next();
172
- if (result.length > 0) {
173
- if (response.headers.get('content-type')?.includes('application/json')) {
174
- try {
175
- result = JSON.parse(result);
176
- }
177
- catch (err) {
178
- this.debugger?.warn('SeyfertError parsing result error (', result, ')', err);
179
- await this.notifyFailRequest(method, finalUrl, err, response.status);
180
- reject(err);
181
- return;
182
- }
183
- }
184
- }
185
- const parsedError = this.parseError(method, route, response, result, originTrace);
221
+ const parsedError = this.parseError(method, route, response, errorResult, originTrace);
186
222
  this.debugger?.warn(parsedError.message);
187
223
  await this.notifyFailRequest(method, finalUrl, parsedError, response.status);
188
224
  reject(parsedError);
189
225
  return;
190
226
  }
191
- if (result.length > 0) {
192
- if (response.headers.get('content-type')?.includes('application/json')) {
193
- try {
194
- result = JSON.parse(result);
195
- }
196
- catch (err) {
197
- this.debugger?.warn('Failed parsing result (', result, ')', err);
198
- await this.notifyFailRequest(method, finalUrl, err, response.status);
199
- next();
200
- reject(err);
201
- return;
202
- }
203
- }
204
- }
205
- await this.notifySuccessRequest(method, finalUrl, response);
227
+ await this.notifySuccessRequest(method, finalUrl, observerResponse);
206
228
  next();
207
- return resolve(result || undefined);
229
+ return resolve(result);
208
230
  };
209
231
  return new Promise((resolve, reject) => {
210
- if (this.globalBlock && auth) {
211
- this.readyQueue.push(() => {
212
- if (!this.ratelimits.has(route)) {
213
- this.ratelimits.set(route, new bucket_1.Bucket(1));
214
- }
215
- this.ratelimits.get(route).push({ next: callback, resolve, reject }, request.unshift);
216
- });
232
+ const dispatch = () => {
233
+ bucketRoute = this.bucketAliases.get(provisionalRoute) ?? bucketRoute;
234
+ if (!this.ratelimits.has(bucketRoute)) {
235
+ this.ratelimits.set(bucketRoute, new bucket_1.Bucket(1));
236
+ }
237
+ this.ratelimits.get(bucketRoute).push({ next: callback, resolve, reject }, requestOptions.unshift);
238
+ };
239
+ const globalQueue = this.globalBlocks.get(this.globalRateLimitKey({ ...requestOptions, auth }));
240
+ if (globalQueue && !url.startsWith('/interactions/')) {
241
+ globalQueue.push(dispatch);
217
242
  }
218
243
  else {
219
- if (!this.ratelimits.has(route)) {
220
- this.ratelimits.set(route, new bucket_1.Bucket(1));
221
- }
222
- this.ratelimits.get(route).push({ next: callback, resolve, reject }, request.unshift);
244
+ dispatch();
223
245
  }
224
246
  });
225
247
  }
248
+ learnBucket(provisionalRoute, currentRoute, url, response, hasRouteOverride) {
249
+ const hash = response.headers.get('x-ratelimit-bucket');
250
+ if (!hash || hasRouteOverride)
251
+ return currentRoute;
252
+ const learnedRoute = `${hash}:${majorParameter(url)}`;
253
+ this.bucketAliases.set(provisionalRoute, learnedRoute);
254
+ const currentBucket = this.ratelimits.get(currentRoute);
255
+ const learnedBucket = this.ratelimits.get(learnedRoute);
256
+ if (!learnedBucket) {
257
+ this.ratelimits.set(learnedRoute, currentBucket);
258
+ return learnedRoute;
259
+ }
260
+ if (learnedBucket !== currentBucket && currentBucket.queue.length) {
261
+ learnedBucket.queue.push(...currentBucket.queue.splice(0));
262
+ }
263
+ return learnedRoute;
264
+ }
265
+ globalRateLimitKey(request) {
266
+ if (request.auth === false)
267
+ return 'unauthenticated';
268
+ return `${this.options.type}:${request.token || this.options.token}`;
269
+ }
270
+ blockGlobal(request, retryAfter) {
271
+ const key = this.globalRateLimitKey(request);
272
+ if (!this.globalBlocks.has(key))
273
+ this.globalBlocks.set(key, []);
274
+ const previousTimer = this.globalTimers.get(key);
275
+ if (previousTimer)
276
+ clearTimeout(previousTimer);
277
+ this.globalBlock = true;
278
+ this.globalTimers.set(key, setTimeout(() => this.unblockGlobal(key), retryAfter || 1));
279
+ }
280
+ unblockGlobal(key) {
281
+ this.globalTimers.delete(key);
282
+ const queue = this.globalBlocks.get(key) ?? [];
283
+ this.globalBlocks.delete(key);
284
+ this.globalBlock = this.globalBlocks.size > 0;
285
+ for (const callback of queue)
286
+ callback();
287
+ }
226
288
  parseError(method, route, response, result, originTrace) {
227
289
  let errMessage = '';
228
290
  let code;
@@ -279,29 +341,50 @@ class ApiHandler {
279
341
  }
280
342
  return errors;
281
343
  }
282
- async handle50X(method, url, request, next) {
344
+ async handle50X(method, url, request, attempts, next = () => { }, resolve, reject) {
345
+ const retryAttempt = typeof attempts === 'number' ? attempts : 0;
346
+ const callback = typeof attempts === 'function' ? attempts : next;
347
+ const requestOptions = {
348
+ ...request,
349
+ unshift: true,
350
+ ...(retryAttempt > 0 ? { _50xRetries: retryAttempt } : {}),
351
+ };
283
352
  const wait = Math.floor(Math.random() * 1900 + 100);
284
353
  this.debugger?.warn(`Handling a 50X status, retrying in ${wait}ms`);
285
- next();
354
+ callback();
286
355
  await (0, common_1.delay)(wait);
287
- return this.request(method, url, {
288
- body: request.body,
289
- auth: request.auth,
290
- reason: request.reason,
291
- route: request.route,
292
- unshift: true,
356
+ return this.request(method, url, requestOptions)
357
+ .then(value => {
358
+ resolve?.(value);
359
+ return value;
360
+ })
361
+ .catch(error => {
362
+ reject?.(error);
293
363
  });
294
364
  }
295
365
  async handle429(route, method, url, request, response, result, next, reject, now) {
296
- await this.onRatelimit?.(response, request);
297
366
  const bucket = this.ratelimits.get(route);
298
367
  let retryAfter;
299
- const data = JSON.parse(result);
300
- if (data.retry_after)
301
- retryAfter = Math.ceil(data.retry_after * 1000);
302
- retryAfter ??=
303
- Number(response.headers.get('x-ratelimit-reset-after') || response.headers.get('retry-after')) * 1000;
304
- if (Number.isNaN(retryAfter)) {
368
+ let data;
369
+ try {
370
+ const parsed = JSON.parse(result);
371
+ if (isPlainObject(parsed))
372
+ data = parsed;
373
+ }
374
+ catch { }
375
+ if (typeof data?.retry_after === 'number') {
376
+ const value = parseRateLimitDelay(data.retry_after);
377
+ if (value !== undefined && value <= MAX_TIMER_DELAY_MS)
378
+ retryAfter = Math.ceil(value);
379
+ }
380
+ for (const header of ['retry-after', 'x-ratelimit-reset-after']) {
381
+ if (retryAfter !== undefined)
382
+ break;
383
+ const value = parseRateLimitDelay(response.headers.get(header));
384
+ if (value !== undefined && value <= MAX_TIMER_DELAY_MS)
385
+ retryAfter = value;
386
+ }
387
+ if (retryAfter === undefined) {
305
388
  this.debugger?.warn(`${route} Could not extract retry_after from 429 response. ${result}`);
306
389
  next();
307
390
  reject(new common_1.SeyfertError('INVALID_RETRY_AFTER', {
@@ -317,71 +400,75 @@ class ApiHandler {
317
400
  }));
318
401
  return false;
319
402
  }
403
+ if (data?.global === true ||
404
+ response.headers.has('x-ratelimit-global') ||
405
+ response.headers.get('x-ratelimit-scope') === 'global') {
406
+ this.blockGlobal(request, retryAfter);
407
+ }
408
+ await this.onRatelimit?.(response, request);
320
409
  if (this.debugger) {
321
410
  const content = `${JSON.stringify(request)} `;
322
411
  this.debugger.info(`${response.headers.get('x-ratelimit-global') ? 'Global' : 'Unexpected'} 429: ${result.slice(0, 256)}\n${content} ${now} ${route} ${response.status}: ${bucket.remaining}/${bucket.limit} left | Reset ${retryAfter} (${bucket.reset - now}ms left) | Scope ${response.headers.get('x-ratelimit-scope')}`);
323
412
  }
324
- if (retryAfter) {
413
+ if (retryAfter)
325
414
  await (0, common_1.delay)(retryAfter);
326
- next();
327
- return this.request(method, url, {
328
- body: request.body,
329
- auth: request.auth,
330
- reason: request.reason,
331
- route: request.route,
332
- unshift: true,
333
- });
334
- }
335
- next();
336
- return this.request(method, url, {
337
- body: request.body,
338
- auth: request.auth,
339
- reason: request.reason,
340
- route: request.route,
415
+ const retry = this.request(method, url, {
416
+ ...request,
341
417
  unshift: true,
342
418
  });
419
+ next();
420
+ return retry.catch(reject);
343
421
  }
344
422
  clearResetInterval(route) {
345
423
  clearInterval(this.ratelimits.get(route).processingResetAfter);
346
424
  this.ratelimits.get(route).processingResetAfter = undefined;
347
425
  this.ratelimits.get(route).resetAfter = 0;
348
426
  }
349
- setResetBucket(route, resp, now, headerNow) {
350
- const retryAfter = Number(resp.headers.get('x-ratelimit-reset-after') || resp.headers.get('retry-after')) * 1000;
351
- if (retryAfter >= 0) {
352
- if (resp.headers.get('x-ratelimit-global')) {
353
- this.globalBlock = true;
354
- setTimeout(() => this.globalUnblock(), retryAfter || 1);
355
- }
356
- else {
357
- this.ratelimits.get(route).reset = (retryAfter || 1) + now;
358
- }
427
+ setResetBucket(route, resp, now, headerNow, normalizedRoute = route) {
428
+ const bucket = this.ratelimits.get(route);
429
+ const retryAfterHeader = resp.headers.get('x-ratelimit-reset-after') ?? resp.headers.get('retry-after');
430
+ const retryAfter = parseRateLimitDelay(retryAfterHeader);
431
+ if (retryAfter !== undefined) {
432
+ bucket.reset = Math.min(retryAfter || 1, MAX_BUCKET_RESET_DELAY_MS) + now;
359
433
  }
360
- else if (resp.headers.get('x-ratelimit-reset')) {
361
- let resetTime = +resp.headers.get('x-ratelimit-reset') * 1000;
362
- if (route.endsWith('/reactions/:id') && +resp.headers.get('x-ratelimit-reset') * 1000 - headerNow === 1000) {
434
+ else {
435
+ const resetHeader = resp.headers.get('x-ratelimit-reset');
436
+ const parsedReset = parseRateLimitDelay(resetHeader);
437
+ if (parsedReset === undefined) {
438
+ if (retryAfterHeader === null && resetHeader === null)
439
+ bucket.reset = now;
440
+ return;
441
+ }
442
+ let resetTime = parsedReset;
443
+ if (normalizedRoute.endsWith('/reactions/:id') && resetTime - headerNow === 1000) {
363
444
  resetTime = now + 250;
364
445
  }
365
- this.ratelimits.get(route).reset = Math.max(resetTime, now);
366
- }
367
- else {
368
- this.ratelimits.get(route).reset = now;
446
+ bucket.reset = Math.min(Math.max(resetTime, now), now + MAX_BUCKET_RESET_DELAY_MS);
369
447
  }
370
448
  }
371
449
  setRatelimitsBucket(route, resp) {
372
- if (resp.headers.has('x-ratelimit-limit')) {
373
- this.ratelimits.get(route).limit = +resp.headers.get('x-ratelimit-limit');
450
+ const bucket = this.ratelimits.get(route);
451
+ const limit = parseRateLimitCount(resp.headers.get('x-ratelimit-limit'), 1);
452
+ const remaining = parseRateLimitCount(resp.headers.get('x-ratelimit-remaining'), 0);
453
+ if (limit !== null && limit !== undefined) {
454
+ bucket.limit = limit;
455
+ bucket.remaining = Math.min(bucket.remaining, limit);
456
+ }
457
+ if (remaining === undefined) {
458
+ bucket.remaining = Math.min(1, bucket.limit);
459
+ }
460
+ else if (remaining !== null) {
461
+ bucket.remaining = Math.min(remaining, bucket.limit);
374
462
  }
375
- this.ratelimits.get(route).remaining =
376
- resp.headers.get('x-ratelimit-remaining') === undefined ? 1 : +resp.headers.get('x-ratelimit-remaining');
377
463
  if (this.options.smartBucket) {
378
- if (resp.headers.has('x-ratelimit-reset-after') &&
379
- !this.ratelimits.get(route).resetAfter &&
380
- Number(resp.headers.get('x-ratelimit-limit')) === Number(resp.headers.get('x-ratelimit-remaining')) + 1) {
381
- this.ratelimits.get(route).resetAfter = +resp.headers.get('x-ratelimit-reset-after') * 1000;
464
+ if (typeof limit === 'number' && typeof remaining === 'number' && !bucket.resetAfter && limit === remaining + 1) {
465
+ const resetAfter = parseRateLimitDelay(resp.headers.get('x-ratelimit-reset-after'));
466
+ if (resetAfter !== undefined && resetAfter <= MAX_SMART_BUCKET_RESET_AFTER_MS) {
467
+ bucket.resetAfter = resetAfter;
468
+ }
382
469
  }
383
- if (this.ratelimits.get(route).resetAfter && !this.ratelimits.get(route).remaining) {
384
- this.ratelimits.get(route).triggerResetAfter();
470
+ if (bucket.resetAfter && !bucket.remaining) {
471
+ bucket.triggerResetAfter();
385
472
  }
386
473
  }
387
474
  }
@@ -394,8 +481,12 @@ class ApiHandler {
394
481
  if (options.request.query) {
395
482
  const params = new URLSearchParams();
396
483
  for (const [key, value] of Object.entries(options.request.query)) {
484
+ if (value === null || value === undefined)
485
+ continue;
397
486
  if (Array.isArray(value)) {
398
487
  for (const item of value) {
488
+ if (item === null || item === undefined)
489
+ continue;
399
490
  params.append(key, String(item));
400
491
  }
401
492
  }
@@ -403,7 +494,9 @@ class ApiHandler {
403
494
  params.append(key, String(value));
404
495
  }
405
496
  }
406
- finalUrl += `?${params}`;
497
+ const query = params.toString();
498
+ if (query)
499
+ finalUrl += `${finalUrl.includes('?') ? '&' : '?'}${query}`;
407
500
  }
408
501
  if (options.request.files?.length || options.request.appendToFormData) {
409
502
  const formData = new FormData();
@@ -471,3 +564,39 @@ class ApiHandler {
471
564
  }
472
565
  }
473
566
  exports.ApiHandler = ApiHandler;
567
+ function isPlainObject(value) {
568
+ if (typeof value !== 'object' || value === null)
569
+ return false;
570
+ const prototype = Object.getPrototypeOf(value);
571
+ return prototype === Object.prototype || prototype === null;
572
+ }
573
+ function normalizeErrorResult(result) {
574
+ if (typeof result === 'string' || isPlainObject(result))
575
+ return result;
576
+ return JSON.stringify(result) ?? String(result);
577
+ }
578
+ async function decodeResponse(response) {
579
+ const contentType = response.headers.get('content-type')?.split(';', 1)[0]?.trim().toLowerCase();
580
+ if (response.status < 300 && contentType && !contentType.startsWith('text/') && !isJsonContentType(contentType)) {
581
+ return response.arrayBuffer();
582
+ }
583
+ const text = await response.text();
584
+ if (!text.length)
585
+ return undefined;
586
+ return isJsonContentType(contentType) ? JSON.parse(text) : text;
587
+ }
588
+ function isJsonContentType(contentType) {
589
+ return contentType === 'application/json' || contentType?.endsWith('+json') === true;
590
+ }
591
+ function majorParameter(url) {
592
+ const channel = url.match(/^\/channels\/([^/]+)/)?.[1];
593
+ if (channel)
594
+ return `channels:${channel}`;
595
+ const guild = url.match(/^\/guilds\/([^/]+)/)?.[1];
596
+ if (guild)
597
+ return `guilds:${guild}`;
598
+ const webhook = url.match(/^\/webhooks\/([^/]+)(?:\/([^/?]+))?/)?.slice(1);
599
+ if (webhook?.[0])
600
+ return `webhooks:${webhook.filter(Boolean).join(':')}`;
601
+ return '';
602
+ }
package/lib/api/bucket.js CHANGED
@@ -35,7 +35,7 @@ class Bucket {
35
35
  this.processing = setTimeout(() => {
36
36
  this.processing = false;
37
37
  this.process(true);
38
- }, this.resetAfter ? 0.5 : Math.max(0, (this.reset || 0) - now) + 1);
38
+ }, Math.max(0, (this.reset || 0) - now) + 1);
39
39
  return;
40
40
  }
41
41
  --this.remaining;
@@ -60,12 +60,18 @@ class Bucket {
60
60
  this.process();
61
61
  }
62
62
  triggerResetAfter() {
63
- if (!this.processingResetAfter && this.resetAfter) {
64
- this.processingResetAfter = setTimeout(() => {
65
- this.remaining++;
66
- this.processingResetAfter = undefined;
67
- }, this.resetAfter * 1.5);
68
- }
63
+ if (this.processingResetAfter || !this.resetAfter)
64
+ return;
65
+ this.processingResetAfter = setTimeout(() => {
66
+ this.remaining++;
67
+ this.processingResetAfter = undefined;
68
+ const resetTimer = this.processing;
69
+ if (!this.queue.length || !resetTimer || resetTimer === true)
70
+ return;
71
+ clearTimeout(resetTimer);
72
+ this.processing = false;
73
+ this.process(true);
74
+ }, this.resetAfter * 1.5);
69
75
  }
70
76
  }
71
77
  exports.Bucket = Bucket;
@@ -3,6 +3,8 @@ export * from './api';
3
3
  export * from './utils/constants';
4
4
  export * from './utils/types';
5
5
  export { calculateUserDefaultAvatarIndex } from './utils/utils';
6
+ /** JSON body accepted by the REST transport, including top-level bulk arrays. */
7
+ export type ApiRequestBody = Record<string, any> | readonly unknown[];
6
8
  export interface ApiHandlerOptions {
7
9
  baseUrl?: string;
8
10
  domain?: string;
@@ -23,7 +25,7 @@ export interface RawFile {
23
25
  filename: string;
24
26
  }
25
27
  export interface ApiRequestOptions {
26
- body?: Record<string, any>;
28
+ body?: ApiRequestBody;
27
29
  query?: Record<string, any>;
28
30
  files?: RawFile[];
29
31
  auth?: boolean;
@@ -1,4 +1,4 @@
1
- export declare const DefaultUserAgent: "DiscordBot (https://seyfert.dev, v4.4.0)";
1
+ export declare const DefaultUserAgent: "DiscordBot (https://seyfert.dev, v4.6.0)";
2
2
  export declare const ALLOWED_EXTENSIONS: readonly ["webp", "png", "jpg", "jpeg", "gif"];
3
3
  export declare const ALLOWED_STICKER_EXTENSIONS: readonly ["png", "json", "gif"];
4
4
  export declare const ALLOWED_SIZES: readonly [16, 32, 64, 100, 128, 256, 512, 1024, 2048, 4096];
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ALLOWED_TAG_BADGE_EXTENSIONS = exports.ALLOWED_SOUNDS_EXTENSIONS = exports.ALLOWED_SIZES = exports.ALLOWED_STICKER_EXTENSIONS = exports.ALLOWED_EXTENSIONS = exports.DefaultUserAgent = void 0;
4
- exports.DefaultUserAgent = 'DiscordBot (https://seyfert.dev, v4.4.0)';
4
+ exports.DefaultUserAgent = 'DiscordBot (https://seyfert.dev, v4.6.0)';
5
5
  exports.ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'];
6
6
  exports.ALLOWED_STICKER_EXTENSIONS = ['png', 'json', 'gif'];
7
7
  exports.ALLOWED_SIZES = [16, 32, 64, 100, 128, 256, 512, 1_024, 2_048, 4_096];
@@ -1,7 +1,7 @@
1
1
  import type { RawFile, UsingClient } from '..';
2
2
  import { type ImageResolvable, type ObjectToLower } from '../common';
3
3
  import { Base } from '../structures/extra/Base';
4
- import type { APIAttachment, RESTAPIAttachment } from '../types';
4
+ import { type APIAttachment, type RESTAPIAttachment } from '../types';
5
5
  export interface AttachmentResolvableMap {
6
6
  url: string;
7
7
  buffer: ReadableStream | Buffer | ArrayBuffer | Uint8Array | Uint8ClampedArray;
@@ -12,6 +12,7 @@ export type AttachmentDataType = keyof AttachmentResolvableMap;
12
12
  export interface AttachmentData {
13
13
  filename: string;
14
14
  description: string;
15
+ is_spoiler?: boolean;
15
16
  resolvable: AttachmentResolvable;
16
17
  type: AttachmentDataType;
17
18
  }
@@ -21,6 +22,10 @@ export declare class Attachment extends Base {
21
22
  data: APIAttachment;
22
23
  constructor(client: UsingClient, data: APIAttachment);
23
24
  toJSON(): APIAttachment;
25
+ /**
26
+ * Gets whether the attachment is a spoiler.
27
+ */
28
+ get spoiler(): boolean;
24
29
  }
25
30
  export declare class AttachmentBuilder {
26
31
  data: Partial<AttachmentData>;
@@ -15,6 +15,7 @@ const node_path_1 = __importDefault(require("node:path"));
15
15
  const utils_1 = require("../api/utils/utils");
16
16
  const common_1 = require("../common");
17
17
  const Base_1 = require("../structures/extra/Base");
18
+ const types_1 = require("../types");
18
19
  class Attachment extends Base_1.Base {
19
20
  data;
20
21
  constructor(client, data) {
@@ -25,6 +26,12 @@ class Attachment extends Base_1.Base {
25
26
  toJSON() {
26
27
  return { ...this.data };
27
28
  }
29
+ /**
30
+ * Gets whether the attachment is a spoiler.
31
+ */
32
+ get spoiler() {
33
+ return Boolean((this.data.flags ?? 0) & types_1.AttachmentFlags.IsSpoiler) || this.data.filename.startsWith('SPOILER_');
34
+ }
28
35
  }
29
36
  exports.Attachment = Attachment;
30
37
  class AttachmentBuilder {
@@ -83,20 +90,17 @@ class AttachmentBuilder {
83
90
  * attachment.setSpoiler(true);
84
91
  */
85
92
  setSpoiler(spoiler) {
86
- if (spoiler === this.spoiler)
87
- return this;
88
- if (!spoiler) {
93
+ this.data.is_spoiler = spoiler;
94
+ if (!spoiler && this.data.filename?.startsWith('SPOILER_')) {
89
95
  this.data.filename = this.data.filename.slice('SPOILER_'.length);
90
- return this;
91
96
  }
92
- this.data.filename = `SPOILER_${this.data.filename}`;
93
97
  return this;
94
98
  }
95
99
  /**
96
100
  * Gets whether the attachment is a spoiler.
97
101
  */
98
102
  get spoiler() {
99
- return this.data.filename?.startsWith('SPOILER_') ?? false;
103
+ return this.data.is_spoiler ?? this.data.filename?.startsWith('SPOILER_') ?? false;
100
104
  }
101
105
  /**
102
106
  * Converts the Attachment instance to JSON.
@@ -116,10 +120,10 @@ function resolveAttachment(resolve) {
116
120
  if ('id' in resolve)
117
121
  return resolve;
118
122
  if (resolve instanceof AttachmentBuilder) {
119
- const { filename, description } = resolve.toJSON();
120
- return { filename, description };
123
+ const { filename, description, is_spoiler } = resolve.toJSON();
124
+ return { filename, description, is_spoiler };
121
125
  }
122
- return { filename: resolve.filename, description: resolve.description };
126
+ return { filename: resolve.filename, description: resolve.description, is_spoiler: resolve.is_spoiler };
123
127
  }
124
128
  /**
125
129
  * Resolves a list of attachments to raw files.
@@ -65,7 +65,7 @@ class CheckboxGroup extends Base_1.BaseComponentBuilder {
65
65
  ...(0, common_1.createValidationMetadata)('number of options between 2 and 10', optionCount, {
66
66
  component: 'CheckboxGroup',
67
67
  }),
68
- detail: 'CheckboxGroup must have between 2 and 10 options.',
68
+ detail: `CheckboxGroup has an invalid options length: expected number of options between 2 and 10, received ${optionCount}.`,
69
69
  },
70
70
  });
71
71
  }