@discordjs/rest
Advanced tools
Comparing version 1.7.1 to 2.0.0
@@ -5,2 +5,35 @@ # Changelog | ||
# [@discordjs/rest@2.0.0](https://github.com/discordjs/discord.js/compare/@discordjs/rest@1.7.1...@discordjs/rest@2.0.0) - (2023-07-31) | ||
## Features | ||
- No-de-no-de, now with extra buns (#9683) ([386f206](https://github.com/discordjs/discord.js/commit/386f206caf74a04c426799af9796ca96dcb37056)) | ||
- **BREAKING CHANGE:** The REST and RequestManager classes now extend AsyncEventEmitter | ||
from `@vladfrangu/async_event_emitter`, which aids in cross-compatibility | ||
between Node, Deno, Bun, CF Workers, Vercel Functions, etc. | ||
- **BREAKING CHANGE:** DefaultUserAgentAppendix has been adapted to support multiple | ||
different platforms (previously mentioned Deno, Bun, CF Workers, etc) | ||
- **BREAKING CHANGE:** the entry point for `@discordjs/rest` will now differ | ||
in non-node-like environments (CF Workers, etc.) | ||
- **Co-authored-by:** Suneet Tipirneni <77477100+suneettipirneni@users.noreply.github.com> | ||
- **Co-authored-by:** Jiralite <33201955+Jiralite@users.noreply.github.com> | ||
- **Co-authored-by:** suneettipirneni <suneettipirneni@icloud.com> | ||
- User avatar decorations (#8914) ([8d97017](https://github.com/discordjs/discord.js/commit/8d9701745840e23854e8f0b057d21cb10e7d1d54)) | ||
- Support new username system (#9512) ([1ab60f9](https://github.com/discordjs/discord.js/commit/1ab60f9da4d6b7ea144fa05b97b029a4bfaeede2)) | ||
## Refactor | ||
- **REST:** Remove double classing (#9722) ([8f4256d](https://github.com/discordjs/discord.js/commit/8f4256db8a52ac08359d0b3436f41b641ac4e382)) | ||
- **BREAKING CHANGE:** `REST` and `RequestManager` have been combined, most of the properties, methods, and events from both classes can now be found on `REST` | ||
- **BREAKING CHANGE:** `REST#raw` has been removed in favor of `REST#queueRequest` | ||
- **BREAKING CHANGE:** `REST#getAgent` has been removed in favor of `REST#agent` | ||
* chore: update for /rest changes | ||
- **rest:** Switch api to fetch-like and provide strategies (#9416) ([cdaa0a3](https://github.com/discordjs/discord.js/commit/cdaa0a36f586459f1e5ede868c4250c7da90455c)) | ||
- **BREAKING CHANGE:** NodeJS v18+ is required when using node due to the use of global `fetch` | ||
- **BREAKING CHANGE:** The raw method of REST now returns a web compatible `Respone` object. | ||
- **BREAKING CHANGE:** The `parseResponse` utility method has been updated to operate on a web compatible `Response` object. | ||
- **BREAKING CHANGE:** Many underlying internals have changed, some of which were exported. | ||
- **BREAKING CHANGE:** `DefaultRestOptions` used to contain a default `agent`, which is now set to `null` instead. | ||
# [@discordjs/rest@1.7.1](https://github.com/discordjs/discord.js/compare/@discordjs/rest@1.7.0...@discordjs/rest@1.7.1) - (2023-05-01) | ||
@@ -17,2 +50,13 @@ | ||
# [@discordjs/rest@1.7.1](https://github.com/discordjs/discord.js/compare/@discordjs/rest@1.7.0...@discordjs/rest@1.7.1) - (2023-05-01) | ||
## Bug Fixes | ||
- Fix external links (#9313) ([a7425c2](https://github.com/discordjs/discord.js/commit/a7425c29c4f23f1b31f4c6a463107ca9eb7fd7e2)) | ||
## Documentation | ||
- Reference package names properly (#9426) ([d6bca9b](https://github.com/discordjs/discord.js/commit/d6bca9bb4d976dc069a5039250db7d5b3e9142ef)) | ||
- Generate static imports for types with api-extractor ([98a76db](https://github.com/discordjs/discord.js/commit/98a76db482879f79d6bb2fb2e5fc65ac2c34e2d9)) | ||
# [@discordjs/rest@1.7.0](https://github.com/discordjs/discord.js/compare/@discordjs/rest@1.6.0...@discordjs/rest@1.7.0) - (2023-04-01) | ||
@@ -19,0 +63,0 @@ |
@@ -1,6 +0,11 @@ | ||
import { Agent, Dispatcher, request, BodyInit } from 'undici'; | ||
import { Buffer } from 'node:buffer'; | ||
import { EventEmitter } from 'node:events'; | ||
import { URLSearchParams } from 'node:url'; | ||
import { R as ResponseLike, a as RawFile, I as InternalRequest, b as RateLimitData, c as RestEventsMap, H as HashData, d as IHandler, e as RESTOptions, f as RouteLike, g as RequestData } from './types-65527f29.js'; | ||
export { A as APIRequest, m as HandlerRequestData, j as InvalidRequestWarningData, i as RateLimitQueueFilter, k as RequestHeaders, l as RequestMethod, h as RestEvents, n as RouteData } from './types-65527f29.js'; | ||
import * as url from 'url'; | ||
import { Snowflake } from 'discord-api-types/v10'; | ||
import * as undici from 'undici'; | ||
import { Dispatcher } from 'undici'; | ||
import { Collection } from '@discordjs/collection'; | ||
import { AsyncEventEmitter } from '@vladfrangu/async_event_emitter'; | ||
import 'node:stream'; | ||
import 'node:stream/web'; | ||
@@ -13,3 +18,3 @@ declare const DefaultUserAgent: `DiscordBot (https://discord.js.org, ${string})`; | ||
declare const DefaultRestOptions: { | ||
readonly agent: Agent; | ||
readonly agent: null; | ||
readonly api: "https://discord.com/api"; | ||
@@ -30,2 +35,3 @@ readonly authPrefix: "Bot"; | ||
readonly handlerSweepInterval: 3600000; | ||
readonly makeRequest: (url: string, init: undici.RequestInit) => Promise<ResponseLike>; | ||
}; | ||
@@ -128,2 +134,10 @@ /** | ||
/** | ||
* Generates a user avatar decoration URL. | ||
* | ||
* @param userId - The id of the user | ||
* @param userAvatarDecoration - The hash provided by Discord for this avatar decoration | ||
* @param options - Optional options for the avatar decoration | ||
*/ | ||
avatarDecoration(userId: string, userAvatarDecoration: string, options?: Readonly<BaseImageURLOptions>): string; | ||
/** | ||
* Generates a banner URL, e.g. for a user or a guild. | ||
@@ -145,7 +159,10 @@ * | ||
/** | ||
* Generates the default avatar URL for a discriminator. | ||
* Generates a default avatar URL | ||
* | ||
* @param discriminator - The discriminator modulo 5 | ||
* @param index - The default avatar index | ||
* @remarks | ||
* To calculate the index for a user do `(userId >> 22) % 6`, | ||
* or `discriminator % 5` if they're using the legacy username system. | ||
*/ | ||
defaultAvatar(discriminator: number): string; | ||
defaultAvatar(index: number): string; | ||
/** | ||
@@ -257,246 +274,124 @@ * Generates a discovery splash URL for a guild's discovery splash. | ||
interface IHandler { | ||
/** | ||
* The unique id of the handler | ||
*/ | ||
readonly id: string; | ||
/** | ||
* If the bucket is currently inactive (no pending requests) | ||
*/ | ||
get inactive(): boolean; | ||
/** | ||
* Queues a request to be sent | ||
* | ||
* @param routeId - The generalized api route with literal ids for major parameters | ||
* @param url - The url to do the request on | ||
* @param options - All the information needed to make a request | ||
* @param requestData - Extra data from the user's request needed for errors and additional processing | ||
*/ | ||
queueRequest(routeId: RouteData, url: string, options: RequestOptions, requestData: HandlerRequestData): Promise<Dispatcher.ResponseData>; | ||
interface DiscordErrorFieldInformation { | ||
code: string; | ||
message: string; | ||
} | ||
interface DiscordErrorGroupWrapper { | ||
_errors: DiscordError[]; | ||
} | ||
type DiscordError = DiscordErrorFieldInformation | DiscordErrorGroupWrapper | string | { | ||
[k: string]: DiscordError; | ||
}; | ||
interface DiscordErrorData { | ||
code: number; | ||
errors?: DiscordError; | ||
message: string; | ||
} | ||
interface OAuthErrorData { | ||
error: string; | ||
error_description?: string; | ||
} | ||
interface RequestBody { | ||
files: RawFile[] | undefined; | ||
json: unknown | undefined; | ||
} | ||
/** | ||
* Options to be passed when creating the REST instance | ||
* Represents an API error returned by Discord | ||
*/ | ||
interface RESTOptions { | ||
declare class DiscordAPIError extends Error { | ||
rawError: DiscordErrorData | OAuthErrorData; | ||
code: number | string; | ||
status: number; | ||
method: string; | ||
url: string; | ||
requestBody: RequestBody; | ||
/** | ||
* The agent to set globally | ||
* @param rawError - The error reported by Discord | ||
* @param code - The error code reported by Discord | ||
* @param status - The status code of the response | ||
* @param method - The method of the request that erred | ||
* @param url - The url of the request that erred | ||
* @param bodyData - The unparsed data for the request that errored | ||
*/ | ||
agent: Dispatcher; | ||
constructor(rawError: DiscordErrorData | OAuthErrorData, code: number | string, status: number, method: string, url: string, bodyData: Pick<InternalRequest, 'body' | 'files'>); | ||
/** | ||
* The base api path, without version | ||
* | ||
* @defaultValue `'https://discord.com/api'` | ||
* The name of the error | ||
*/ | ||
api: string; | ||
/** | ||
* The authorization prefix to use for requests, useful if you want to use | ||
* bearer tokens | ||
* | ||
* @defaultValue `'Bot'` | ||
*/ | ||
authPrefix: 'Bearer' | 'Bot'; | ||
/** | ||
* The cdn path | ||
* | ||
* @defaultValue 'https://cdn.discordapp.com' | ||
*/ | ||
cdn: string; | ||
/** | ||
* How many requests to allow sending per second (Infinity for unlimited, 50 for the standard global limit used by Discord) | ||
* | ||
* @defaultValue `50` | ||
*/ | ||
globalRequestsPerSecond: number; | ||
/** | ||
* The amount of time in milliseconds that passes between each hash sweep. (defaults to 1h) | ||
* | ||
* @defaultValue `3_600_000` | ||
*/ | ||
handlerSweepInterval: number; | ||
/** | ||
* The maximum amount of time a hash can exist in milliseconds without being hit with a request (defaults to 24h) | ||
* | ||
* @defaultValue `86_400_000` | ||
*/ | ||
hashLifetime: number; | ||
/** | ||
* The amount of time in milliseconds that passes between each hash sweep. (defaults to 4h) | ||
* | ||
* @defaultValue `14_400_000` | ||
*/ | ||
hashSweepInterval: number; | ||
/** | ||
* Additional headers to send for all API requests | ||
* | ||
* @defaultValue `{}` | ||
*/ | ||
headers: Record<string, string>; | ||
/** | ||
* The number of invalid REST requests (those that return 401, 403, or 429) in a 10 minute window between emitted warnings (0 for no warnings). | ||
* That is, if set to 500, warnings will be emitted at invalid request number 500, 1000, 1500, and so on. | ||
* | ||
* @defaultValue `0` | ||
*/ | ||
invalidRequestWarningInterval: number; | ||
/** | ||
* The extra offset to add to rate limits in milliseconds | ||
* | ||
* @defaultValue `50` | ||
*/ | ||
offset: number; | ||
/** | ||
* Determines how rate limiting and pre-emptive throttling should be handled. | ||
* When an array of strings, each element is treated as a prefix for the request route | ||
* (e.g. `/channels` to match any route starting with `/channels` such as `/channels/:id/messages`) | ||
* for which to throw {@link RateLimitError}s. All other request routes will be queued normally | ||
* | ||
* @defaultValue `null` | ||
*/ | ||
rejectOnRateLimit: RateLimitQueueFilter | string[] | null; | ||
/** | ||
* The number of retries for errors with the 500 code, or errors | ||
* that timeout | ||
* | ||
* @defaultValue `3` | ||
*/ | ||
retries: number; | ||
/** | ||
* The time to wait in milliseconds before a request is aborted | ||
* | ||
* @defaultValue `15_000` | ||
*/ | ||
timeout: number; | ||
/** | ||
* Extra information to add to the user agent | ||
* | ||
* @defaultValue DefaultUserAgentAppendix | ||
*/ | ||
userAgentAppendix: string; | ||
/** | ||
* The version of the API to use | ||
* | ||
* @defaultValue `'10'` | ||
*/ | ||
version: string; | ||
get name(): string; | ||
private static getMessage; | ||
private static flattenDiscordError; | ||
} | ||
/** | ||
* Data emitted on `RESTEvents.RateLimited` | ||
* Represents a HTTP error | ||
*/ | ||
interface RateLimitData { | ||
declare class HTTPError extends Error { | ||
status: number; | ||
method: string; | ||
url: string; | ||
requestBody: RequestBody; | ||
name: string; | ||
/** | ||
* Whether the rate limit that was reached was the global limit | ||
* @param status - The status code of the response | ||
* @param statusText - The status text of the response | ||
* @param method - The method of the request that erred | ||
* @param url - The url of the request that erred | ||
* @param bodyData - The unparsed data for the request that errored | ||
*/ | ||
global: boolean; | ||
/** | ||
* The bucket hash for this request | ||
*/ | ||
hash: string; | ||
/** | ||
* The amount of requests we can perform before locking requests | ||
*/ | ||
constructor(status: number, statusText: string, method: string, url: string, bodyData: Pick<InternalRequest, 'body' | 'files'>); | ||
} | ||
declare class RateLimitError extends Error implements RateLimitData { | ||
timeToReset: number; | ||
limit: number; | ||
/** | ||
* The major parameter of the route | ||
* | ||
* For example, in `/channels/x`, this will be `x`. | ||
* If there is no major parameter (e.g: `/bot/gateway`) this will be `global`. | ||
*/ | ||
majorParameter: string; | ||
/** | ||
* The HTTP method being performed | ||
*/ | ||
method: string; | ||
/** | ||
* The route being hit in this request | ||
*/ | ||
hash: string; | ||
url: string; | ||
route: string; | ||
majorParameter: string; | ||
global: boolean; | ||
constructor({ timeToReset, limit, method, hash, url, route, majorParameter, global }: RateLimitData); | ||
/** | ||
* The time, in milliseconds, until the request-lock is reset | ||
* The name of the error | ||
*/ | ||
timeToReset: number; | ||
/** | ||
* The full URL for this request | ||
*/ | ||
url: string; | ||
get name(): string; | ||
} | ||
/** | ||
* A function that determines whether the rate limit hit should throw an Error | ||
* Represents the class that manages handlers for endpoints | ||
*/ | ||
type RateLimitQueueFilter = (rateLimitData: RateLimitData) => Promise<boolean> | boolean; | ||
interface APIRequest { | ||
declare class REST extends AsyncEventEmitter<RestEventsMap> { | ||
#private; | ||
/** | ||
* The data that was used to form the body of this request | ||
* The {@link https://undici.nodejs.org/#/docs/api/Agent | Agent} for all requests | ||
* performed by this manager. | ||
*/ | ||
data: HandlerRequestData; | ||
agent: Dispatcher | null; | ||
readonly cdn: CDN; | ||
/** | ||
* The HTTP method used in this request | ||
* The number of requests remaining in the global bucket | ||
*/ | ||
method: string; | ||
globalRemaining: number; | ||
/** | ||
* Additional HTTP options for this request | ||
* The promise used to wait out the global rate limit | ||
*/ | ||
options: RequestOptions; | ||
globalDelay: Promise<void> | null; | ||
/** | ||
* The full path used to make the request | ||
* The timestamp at which the global bucket resets | ||
*/ | ||
path: RouteLike; | ||
globalReset: number; | ||
/** | ||
* The number of times this request has been attempted | ||
* API bucket hashes that are cached from provided routes | ||
*/ | ||
retries: number; | ||
readonly hashes: Collection<string, HashData>; | ||
/** | ||
* The API route identifying the ratelimit for this request | ||
* Request handlers created from the bucket hash and the major parameters | ||
*/ | ||
route: string; | ||
} | ||
interface InvalidRequestWarningData { | ||
/** | ||
* Number of invalid requests that have been made in the window | ||
*/ | ||
count: number; | ||
/** | ||
* Time in milliseconds remaining before the count resets | ||
*/ | ||
remainingTime: number; | ||
} | ||
interface RestEvents { | ||
handlerSweep: [sweptHandlers: Collection<string, IHandler>]; | ||
hashSweep: [sweptHashes: Collection<string, HashData>]; | ||
invalidRequestWarning: [invalidRequestInfo: InvalidRequestWarningData]; | ||
newListener: [name: string, listener: (...args: any) => void]; | ||
rateLimited: [rateLimitInfo: RateLimitData]; | ||
removeListener: [name: string, listener: (...args: any) => void]; | ||
response: [request: APIRequest, response: Dispatcher.ResponseData]; | ||
restDebug: [info: string]; | ||
} | ||
type RequestOptions = Exclude<Parameters<typeof request>[1], undefined>; | ||
interface REST { | ||
emit: (<K extends keyof RestEvents>(event: K, ...args: RestEvents[K]) => boolean) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, ...args: any[]) => boolean); | ||
off: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this); | ||
on: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this); | ||
once: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this); | ||
removeAllListeners: (<K extends keyof RestEvents>(event?: K) => this) & (<S extends string | symbol>(event?: Exclude<S, keyof RestEvents>) => this); | ||
} | ||
declare class REST extends EventEmitter { | ||
readonly cdn: CDN; | ||
readonly requestManager: RequestManager; | ||
readonly handlers: Collection<string, IHandler>; | ||
private hashTimer; | ||
private handlerTimer; | ||
readonly options: RESTOptions; | ||
constructor(options?: Partial<RESTOptions>); | ||
private setupSweepers; | ||
/** | ||
* Gets the agent set for this instance | ||
*/ | ||
getAgent(): Dispatcher | null; | ||
/** | ||
* Sets the default agent to use for requests performed by this instance | ||
* | ||
* @param agent - Sets the agent to use | ||
*/ | ||
setAgent(agent: Dispatcher): this; | ||
/** | ||
* Sets the authorization token that should be used for requests | ||
* | ||
* @param token - The authorization token to use | ||
*/ | ||
setToken(token: string): this; | ||
/** | ||
* Runs a get request from the api | ||
@@ -543,184 +438,2 @@ * | ||
/** | ||
* Runs a request from the API, yielding the raw Response object | ||
* | ||
* @param options - Request options | ||
*/ | ||
raw(options: InternalRequest): Promise<Dispatcher.ResponseData>; | ||
} | ||
/** | ||
* Represents a file to be added to the request | ||
*/ | ||
interface RawFile { | ||
/** | ||
* Content-Type of the file | ||
*/ | ||
contentType?: string; | ||
/** | ||
* The actual data for the file | ||
*/ | ||
data: Buffer | boolean | number | string; | ||
/** | ||
* An explicit key to use for key of the formdata field for this file. | ||
* When not provided, the index of the file in the files array is used in the form `files[${index}]`. | ||
* If you wish to alter the placeholder snowflake, you must provide this property in the same form (`files[${placeholder}]`) | ||
*/ | ||
key?: string; | ||
/** | ||
* The name of the file | ||
*/ | ||
name: string; | ||
} | ||
/** | ||
* Represents possible data to be given to an endpoint | ||
*/ | ||
interface RequestData { | ||
/** | ||
* Whether to append JSON data to form data instead of `payload_json` when sending files | ||
*/ | ||
appendToFormData?: boolean; | ||
/** | ||
* If this request needs the `Authorization` header | ||
* | ||
* @defaultValue `true` | ||
*/ | ||
auth?: boolean; | ||
/** | ||
* The authorization prefix to use for this request, useful if you use this with bearer tokens | ||
* | ||
* @defaultValue `'Bot'` | ||
*/ | ||
authPrefix?: 'Bearer' | 'Bot'; | ||
/** | ||
* The body to send to this request. | ||
* If providing as BodyInit, set `passThroughBody: true` | ||
*/ | ||
body?: BodyInit | unknown; | ||
/** | ||
* The {@link https://undici.nodejs.org/#/docs/api/Agent | Agent} to use for the request. | ||
*/ | ||
dispatcher?: Agent; | ||
/** | ||
* Files to be attached to this request | ||
*/ | ||
files?: RawFile[] | undefined; | ||
/** | ||
* Additional headers to add to this request | ||
*/ | ||
headers?: Record<string, string>; | ||
/** | ||
* Whether to pass-through the body property directly to `fetch()`. | ||
* <warn>This only applies when files is NOT present</warn> | ||
*/ | ||
passThroughBody?: boolean; | ||
/** | ||
* Query string parameters to append to the called endpoint | ||
*/ | ||
query?: URLSearchParams; | ||
/** | ||
* Reason to show in the audit logs | ||
*/ | ||
reason?: string | undefined; | ||
/** | ||
* The signal to abort the queue entry or the REST call, where applicable | ||
*/ | ||
signal?: AbortSignal | undefined; | ||
/** | ||
* If this request should be versioned | ||
* | ||
* @defaultValue `true` | ||
*/ | ||
versioned?: boolean; | ||
} | ||
/** | ||
* Possible headers for an API call | ||
*/ | ||
interface RequestHeaders { | ||
Authorization?: string; | ||
'User-Agent': string; | ||
'X-Audit-Log-Reason'?: string; | ||
} | ||
/** | ||
* Possible API methods to be used when doing requests | ||
*/ | ||
declare enum RequestMethod { | ||
Delete = "DELETE", | ||
Get = "GET", | ||
Patch = "PATCH", | ||
Post = "POST", | ||
Put = "PUT" | ||
} | ||
type RouteLike = `/${string}`; | ||
/** | ||
* Internal request options | ||
* | ||
* @internal | ||
*/ | ||
interface InternalRequest extends RequestData { | ||
fullRoute: RouteLike; | ||
method: RequestMethod; | ||
} | ||
type HandlerRequestData = Pick<InternalRequest, 'auth' | 'body' | 'files' | 'signal'>; | ||
/** | ||
* Parsed route data for an endpoint | ||
* | ||
* @internal | ||
*/ | ||
interface RouteData { | ||
bucketRoute: string; | ||
majorParameter: string; | ||
original: RouteLike; | ||
} | ||
/** | ||
* Represents a hash and its associated fields | ||
* | ||
* @internal | ||
*/ | ||
interface HashData { | ||
lastAccess: number; | ||
value: string; | ||
} | ||
interface RequestManager { | ||
emit: (<K extends keyof RestEvents>(event: K, ...args: RestEvents[K]) => boolean) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, ...args: any[]) => boolean); | ||
off: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this); | ||
on: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this); | ||
once: (<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void) => this) & (<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void) => this); | ||
removeAllListeners: (<K extends keyof RestEvents>(event?: K) => this) & (<S extends string | symbol>(event?: Exclude<S, keyof RestEvents>) => this); | ||
} | ||
/** | ||
* Represents the class that manages handlers for endpoints | ||
*/ | ||
declare class RequestManager extends EventEmitter { | ||
#private; | ||
/** | ||
* The {@link https://undici.nodejs.org/#/docs/api/Agent | Agent} for all requests | ||
* performed by this manager. | ||
*/ | ||
agent: Dispatcher | null; | ||
/** | ||
* The number of requests remaining in the global bucket | ||
*/ | ||
globalRemaining: number; | ||
/** | ||
* The promise used to wait out the global rate limit | ||
*/ | ||
globalDelay: Promise<void> | null; | ||
/** | ||
* The timestamp at which the global bucket resets | ||
*/ | ||
globalReset: number; | ||
/** | ||
* API bucket hashes that are cached from provided routes | ||
*/ | ||
readonly hashes: Collection<string, HashData>; | ||
/** | ||
* Request handlers created from the bucket hash and the major parameters | ||
*/ | ||
readonly handlers: Collection<string, IHandler>; | ||
private hashTimer; | ||
private handlerTimer; | ||
readonly options: RESTOptions; | ||
constructor(options: Partial<RESTOptions>); | ||
private setupSweepers; | ||
/** | ||
* Sets the default agent to use for requests performed by this manager | ||
@@ -743,3 +456,3 @@ * | ||
*/ | ||
queueRequest(request: InternalRequest): Promise<Dispatcher.ResponseData>; | ||
queueRequest(request: InternalRequest): Promise<ResponseLike>; | ||
/** | ||
@@ -777,87 +490,3 @@ * Creates a new rate limit handler from a hash, based on the hash and the major parameter | ||
interface DiscordErrorFieldInformation { | ||
code: string; | ||
message: string; | ||
} | ||
interface DiscordErrorGroupWrapper { | ||
_errors: DiscordError[]; | ||
} | ||
type DiscordError = DiscordErrorFieldInformation | DiscordErrorGroupWrapper | string | { | ||
[k: string]: DiscordError; | ||
}; | ||
interface DiscordErrorData { | ||
code: number; | ||
errors?: DiscordError; | ||
message: string; | ||
} | ||
interface OAuthErrorData { | ||
error: string; | ||
error_description?: string; | ||
} | ||
interface RequestBody { | ||
files: RawFile[] | undefined; | ||
json: unknown | undefined; | ||
} | ||
/** | ||
* Represents an API error returned by Discord | ||
*/ | ||
declare class DiscordAPIError extends Error { | ||
rawError: DiscordErrorData | OAuthErrorData; | ||
code: number | string; | ||
status: number; | ||
method: string; | ||
url: string; | ||
requestBody: RequestBody; | ||
/** | ||
* @param rawError - The error reported by Discord | ||
* @param code - The error code reported by Discord | ||
* @param status - The status code of the response | ||
* @param method - The method of the request that erred | ||
* @param url - The url of the request that erred | ||
* @param bodyData - The unparsed data for the request that errored | ||
*/ | ||
constructor(rawError: DiscordErrorData | OAuthErrorData, code: number | string, status: number, method: string, url: string, bodyData: Pick<InternalRequest, 'body' | 'files'>); | ||
/** | ||
* The name of the error | ||
*/ | ||
get name(): string; | ||
private static getMessage; | ||
private static flattenDiscordError; | ||
} | ||
/** | ||
* Represents a HTTP error | ||
*/ | ||
declare class HTTPError extends Error { | ||
status: number; | ||
method: string; | ||
url: string; | ||
requestBody: RequestBody; | ||
name: string; | ||
/** | ||
* @param status - The status code of the response | ||
* @param method - The method of the request that erred | ||
* @param url - The url of the request that erred | ||
* @param bodyData - The unparsed data for the request that errored | ||
*/ | ||
constructor(status: number, method: string, url: string, bodyData: Pick<InternalRequest, 'body' | 'files'>); | ||
} | ||
declare class RateLimitError extends Error implements RateLimitData { | ||
timeToReset: number; | ||
limit: number; | ||
method: string; | ||
hash: string; | ||
url: string; | ||
route: string; | ||
majorParameter: string; | ||
global: boolean; | ||
constructor({ timeToReset, limit, method, hash, url, route, majorParameter, global }: RateLimitData); | ||
/** | ||
* The name of the error | ||
*/ | ||
get name(): string; | ||
} | ||
/** | ||
* Creates and populates an URLSearchParams instance from an object, stripping | ||
@@ -869,3 +498,3 @@ * out null and undefined values, while also coercing non-strings to strings. | ||
*/ | ||
declare function makeURLSearchParams<T extends object>(options?: Readonly<T>): URLSearchParams; | ||
declare function makeURLSearchParams<T extends object>(options?: Readonly<T>): url.URLSearchParams; | ||
/** | ||
@@ -876,3 +505,9 @@ * Converts the response to usable data | ||
*/ | ||
declare function parseResponse(res: Dispatcher.ResponseData): Promise<unknown>; | ||
declare function parseResponse(res: ResponseLike): Promise<unknown>; | ||
/** | ||
* Calculates the default avatar index for a given user id. | ||
* | ||
* @param userId - The user id to calculate the default avatar index for | ||
*/ | ||
declare function calculateUserDefaultAvatarIndex(userId: Snowflake): number; | ||
@@ -885,2 +520,2 @@ /** | ||
export { ALLOWED_EXTENSIONS, ALLOWED_SIZES, ALLOWED_STICKER_EXTENSIONS, APIRequest, BaseImageURLOptions, BurstHandlerMajorIdKey, CDN, DefaultRestOptions, DefaultUserAgent, DefaultUserAgentAppendix, DiscordAPIError, DiscordErrorData, HTTPError, HandlerRequestData, HashData, ImageExtension, ImageSize, ImageURLOptions, InternalRequest, InvalidRequestWarningData, MakeURLOptions, OAuthErrorData, OverwrittenMimeTypes, REST, RESTEvents, RESTOptions, RateLimitData, RateLimitError, RateLimitQueueFilter, RawFile, RequestBody, RequestData, RequestHeaders, RequestManager, RequestMethod, RequestOptions, RestEvents, RouteData, RouteLike, StickerExtension, makeURLSearchParams, parseResponse, version }; | ||
export { ALLOWED_EXTENSIONS, ALLOWED_SIZES, ALLOWED_STICKER_EXTENSIONS, BaseImageURLOptions, BurstHandlerMajorIdKey, CDN, DefaultRestOptions, DefaultUserAgent, DefaultUserAgentAppendix, DiscordAPIError, DiscordErrorData, HTTPError, HashData, ImageExtension, ImageSize, ImageURLOptions, InternalRequest, MakeURLOptions, OAuthErrorData, OverwrittenMimeTypes, REST, RESTEvents, RESTOptions, RateLimitData, RateLimitError, RawFile, RequestBody, RequestData, ResponseLike, RestEventsMap, RouteLike, StickerExtension, calculateUserDefaultAvatarIndex, makeURLSearchParams, parseResponse, version }; |
"use strict"; | ||
var __create = Object.create; | ||
var __defProp = Object.defineProperty; | ||
var __getOwnPropDesc = Object.getOwnPropertyDescriptor; | ||
var __getOwnPropNames = Object.getOwnPropertyNames; | ||
var __getProtoOf = Object.getPrototypeOf; | ||
var __hasOwnProp = Object.prototype.hasOwnProperty; | ||
@@ -21,10 +19,2 @@ var __name = (target, value) => __defProp(target, "name", { value, configurable: true }); | ||
}; | ||
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps( | ||
// If the importer is in node compatibility mode or this is not an ESM | ||
// file that has been converted to a CommonJS file using a Babel- | ||
// compatible transform (i.e. "__esModule" has not been set), then set | ||
// "default" to the CommonJS "module.exports" for node compatibility. | ||
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target, | ||
mod | ||
)); | ||
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); | ||
@@ -49,4 +39,4 @@ | ||
RateLimitError: () => RateLimitError, | ||
RequestManager: () => RequestManager, | ||
RequestMethod: () => RequestMethod, | ||
calculateUserDefaultAvatarIndex: () => calculateUserDefaultAvatarIndex, | ||
makeURLSearchParams: () => makeURLSearchParams, | ||
@@ -57,20 +47,87 @@ parseResponse: () => parseResponse, | ||
module.exports = __toCommonJS(src_exports); | ||
var import_node_buffer = require("buffer"); | ||
var import_util2 = require("@discordjs/util"); | ||
var import_undici2 = require("undici"); | ||
// src/lib/CDN.ts | ||
// src/environment.ts | ||
var defaultStrategy; | ||
function setDefaultStrategy(newStrategy) { | ||
defaultStrategy = newStrategy; | ||
} | ||
__name(setDefaultStrategy, "setDefaultStrategy"); | ||
function getDefaultStrategy() { | ||
return defaultStrategy; | ||
} | ||
__name(getDefaultStrategy, "getDefaultStrategy"); | ||
// src/strategies/undiciRequest.ts | ||
var import_node_http = require("http"); | ||
var import_node_url = require("url"); | ||
var import_node_util = require("util"); | ||
var import_undici = require("undici"); | ||
async function makeRequest(url, init) { | ||
const options = { | ||
...init, | ||
body: await resolveBody(init.body) | ||
}; | ||
const res = await (0, import_undici.request)(url, options); | ||
return { | ||
body: res.body, | ||
async arrayBuffer() { | ||
return res.body.arrayBuffer(); | ||
}, | ||
async json() { | ||
return res.body.json(); | ||
}, | ||
async text() { | ||
return res.body.text(); | ||
}, | ||
get bodyUsed() { | ||
return res.body.bodyUsed; | ||
}, | ||
headers: new import_undici.Headers(res.headers), | ||
status: res.statusCode, | ||
statusText: import_node_http.STATUS_CODES[res.statusCode], | ||
ok: res.statusCode >= 200 && res.statusCode < 300 | ||
}; | ||
} | ||
__name(makeRequest, "makeRequest"); | ||
async function resolveBody(body) { | ||
if (body == null) { | ||
return null; | ||
} else if (typeof body === "string") { | ||
return body; | ||
} else if (import_node_util.types.isUint8Array(body)) { | ||
return body; | ||
} else if (import_node_util.types.isArrayBuffer(body)) { | ||
return new Uint8Array(body); | ||
} else if (body instanceof import_node_url.URLSearchParams) { | ||
return body.toString(); | ||
} else if (body instanceof DataView) { | ||
return new Uint8Array(body.buffer); | ||
} else if (body instanceof Blob) { | ||
return new Uint8Array(await body.arrayBuffer()); | ||
} else if (body instanceof FormData) { | ||
return body; | ||
} else if (body[Symbol.iterator]) { | ||
const chunks = [...body]; | ||
return Buffer.concat(chunks); | ||
} else if (body[Symbol.asyncIterator]) { | ||
const chunks = []; | ||
for await (const chunk of body) { | ||
chunks.push(chunk); | ||
} | ||
return Buffer.concat(chunks); | ||
} | ||
throw new TypeError(`Unable to resolve body.`); | ||
} | ||
__name(resolveBody, "resolveBody"); | ||
// src/lib/utils/constants.ts | ||
var import_node_process = __toESM(require("process")); | ||
var import_util = require("@discordjs/util"); | ||
var import_v10 = require("discord-api-types/v10"); | ||
var import_undici = require("undici"); | ||
var DefaultUserAgent = `DiscordBot (https://discord.js.org, 1.7.1)`; | ||
var DefaultUserAgentAppendix = import_node_process.default.release?.name === "node" ? `Node.js/${import_node_process.default.version}` : ""; | ||
var DefaultUserAgent = `DiscordBot (https://discord.js.org, 2.0.0)`; | ||
var DefaultUserAgentAppendix = (0, import_util.getUserAgentAppendix)(); | ||
var DefaultRestOptions = { | ||
get agent() { | ||
return new import_undici.Agent({ | ||
connect: { | ||
timeout: 3e4 | ||
} | ||
}); | ||
}, | ||
agent: null, | ||
api: "https://discord.com/api", | ||
@@ -92,4 +149,7 @@ authPrefix: "Bot", | ||
// 24 Hours | ||
handlerSweepInterval: 36e5 | ||
handlerSweepInterval: 36e5, | ||
// 1 Hour | ||
async makeRequest(...args) { | ||
return getDefaultStrategy()(...args); | ||
} | ||
}; | ||
@@ -119,2 +179,5 @@ var RESTEvents = /* @__PURE__ */ ((RESTEvents2) => { | ||
} | ||
static { | ||
__name(this, "CDN"); | ||
} | ||
/** | ||
@@ -151,2 +214,12 @@ * Generates an app asset URL for a client's asset. | ||
/** | ||
* Generates a user avatar decoration URL. | ||
* | ||
* @param userId - The id of the user | ||
* @param userAvatarDecoration - The hash provided by Discord for this avatar decoration | ||
* @param options - Optional options for the avatar decoration | ||
*/ | ||
avatarDecoration(userId, userAvatarDecoration, options) { | ||
return this.makeURL(`/avatar-decorations/${userId}/${userAvatarDecoration}`, options); | ||
} | ||
/** | ||
* Generates a banner URL, e.g. for a user or a guild. | ||
@@ -172,8 +245,11 @@ * | ||
/** | ||
* Generates the default avatar URL for a discriminator. | ||
* Generates a default avatar URL | ||
* | ||
* @param discriminator - The discriminator modulo 5 | ||
* @param index - The default avatar index | ||
* @remarks | ||
* To calculate the index for a user do `(userId >> 22) % 6`, | ||
* or `discriminator % 5` if they're using the legacy username system. | ||
*/ | ||
defaultAvatar(discriminator) { | ||
return this.makeURL(`/embed/avatars/${discriminator}`, { extension: "png" }); | ||
defaultAvatar(index) { | ||
return this.makeURL(`/embed/avatars/${index}`, { extension: "png" }); | ||
} | ||
@@ -317,3 +393,3 @@ /** | ||
} | ||
const url = new import_node_url.URL(`${this.base}${route}.${extension}`); | ||
const url = new URL(`${this.base}${route}.${extension}`); | ||
if (size) { | ||
@@ -325,3 +401,2 @@ url.searchParams.set("size", String(size)); | ||
}; | ||
__name(CDN, "CDN"); | ||
@@ -337,3 +412,3 @@ // src/lib/errors/DiscordAPIError.ts | ||
__name(isErrorResponse, "isErrorResponse"); | ||
var DiscordAPIError = class extends Error { | ||
var DiscordAPIError = class _DiscordAPIError extends Error { | ||
/** | ||
@@ -348,3 +423,3 @@ * @param rawError - The error reported by Discord | ||
constructor(rawError, code, status, method, url, bodyData) { | ||
super(DiscordAPIError.getMessage(rawError)); | ||
super(_DiscordAPIError.getMessage(rawError)); | ||
this.rawError = rawError; | ||
@@ -357,2 +432,5 @@ this.code = code; | ||
} | ||
static { | ||
__name(this, "DiscordAPIError"); | ||
} | ||
requestBody; | ||
@@ -363,3 +441,3 @@ /** | ||
get name() { | ||
return `${DiscordAPIError.name}[${this.code}]`; | ||
return `${_DiscordAPIError.name}[${this.code}]`; | ||
} | ||
@@ -395,9 +473,8 @@ static getMessage(error) { | ||
}; | ||
__name(DiscordAPIError, "DiscordAPIError"); | ||
// src/lib/errors/HTTPError.ts | ||
var import_node_http = require("http"); | ||
var HTTPError = class extends Error { | ||
var HTTPError = class _HTTPError extends Error { | ||
/** | ||
* @param status - The status code of the response | ||
* @param statusText - The status text of the response | ||
* @param method - The method of the request that erred | ||
@@ -407,4 +484,4 @@ * @param url - The url of the request that erred | ||
*/ | ||
constructor(status, method, url, bodyData) { | ||
super(import_node_http.STATUS_CODES[status]); | ||
constructor(status, statusText, method, url, bodyData) { | ||
super(statusText); | ||
this.status = status; | ||
@@ -415,9 +492,14 @@ this.method = method; | ||
} | ||
static { | ||
__name(this, "HTTPError"); | ||
} | ||
requestBody; | ||
name = HTTPError.name; | ||
name = _HTTPError.name; | ||
}; | ||
__name(HTTPError, "HTTPError"); | ||
// src/lib/errors/RateLimitError.ts | ||
var RateLimitError = class extends Error { | ||
var RateLimitError = class _RateLimitError extends Error { | ||
static { | ||
__name(this, "RateLimitError"); | ||
} | ||
timeToReset; | ||
@@ -446,31 +528,23 @@ limit; | ||
get name() { | ||
return `${RateLimitError.name}[${this.route}]`; | ||
return `${_RateLimitError.name}[${this.route}]`; | ||
} | ||
}; | ||
__name(RateLimitError, "RateLimitError"); | ||
// src/lib/RequestManager.ts | ||
var import_node_buffer2 = require("buffer"); | ||
var import_node_events = require("events"); | ||
var import_node_timers2 = require("timers"); | ||
// src/lib/REST.ts | ||
var import_collection = require("@discordjs/collection"); | ||
var import_util = require("@discordjs/util"); | ||
var import_snowflake = require("@sapphire/snowflake"); | ||
var import_undici4 = require("undici"); | ||
var import_async_event_emitter = require("@vladfrangu/async_event_emitter"); | ||
var import_magic_bytes = require("magic-bytes.js"); | ||
// src/lib/handlers/BurstHandler.ts | ||
var import_promises = require("timers/promises"); | ||
// src/lib/utils/types.ts | ||
var RequestMethod = /* @__PURE__ */ ((RequestMethod2) => { | ||
RequestMethod2["Delete"] = "DELETE"; | ||
RequestMethod2["Get"] = "GET"; | ||
RequestMethod2["Patch"] = "PATCH"; | ||
RequestMethod2["Post"] = "POST"; | ||
RequestMethod2["Put"] = "PUT"; | ||
return RequestMethod2; | ||
})(RequestMethod || {}); | ||
// src/lib/utils/utils.ts | ||
var import_node_buffer = require("buffer"); | ||
var import_node_url2 = require("url"); | ||
var import_node_util = require("util"); | ||
var import_undici2 = require("undici"); | ||
function parseHeader(header) { | ||
if (header === void 0 || typeof header === "string") { | ||
return header; | ||
} | ||
return header.join(";"); | ||
} | ||
__name(parseHeader, "parseHeader"); | ||
function serializeSearchParam(value) { | ||
@@ -499,3 +573,3 @@ switch (typeof value) { | ||
function makeURLSearchParams(options) { | ||
const params = new import_node_url2.URLSearchParams(); | ||
const params = new URLSearchParams(); | ||
if (!options) | ||
@@ -512,7 +586,6 @@ return params; | ||
async function parseResponse(res) { | ||
const header = parseHeader(res.headers["content-type"]); | ||
if (header?.startsWith("application/json")) { | ||
return res.body.json(); | ||
if (res.headers.get("Content-Type")?.startsWith("application/json")) { | ||
return res.json(); | ||
} | ||
return res.body.arrayBuffer(); | ||
return res.arrayBuffer(); | ||
} | ||
@@ -532,39 +605,2 @@ __name(parseResponse, "parseResponse"); | ||
__name(hasSublimit, "hasSublimit"); | ||
async function resolveBody(body) { | ||
if (body == null) { | ||
return null; | ||
} else if (typeof body === "string") { | ||
return body; | ||
} else if (import_node_util.types.isUint8Array(body)) { | ||
return body; | ||
} else if (import_node_util.types.isArrayBuffer(body)) { | ||
return new Uint8Array(body); | ||
} else if (body instanceof import_node_url2.URLSearchParams) { | ||
return body.toString(); | ||
} else if (body instanceof DataView) { | ||
return new Uint8Array(body.buffer); | ||
} else if (body instanceof import_node_buffer.Blob) { | ||
return new Uint8Array(await body.arrayBuffer()); | ||
} else if (body instanceof import_undici2.FormData) { | ||
return body; | ||
} else if (body[Symbol.iterator]) { | ||
const chunks = [...body]; | ||
const length = chunks.reduce((a, b) => a + b.length, 0); | ||
const uint8 = new Uint8Array(length); | ||
let lengthUsed = 0; | ||
return chunks.reduce((a, b) => { | ||
a.set(b, lengthUsed); | ||
lengthUsed += b.length; | ||
return a; | ||
}, uint8); | ||
} else if (body[Symbol.asyncIterator]) { | ||
const chunks = []; | ||
for await (const chunk of body) { | ||
chunks.push(chunk); | ||
} | ||
return import_node_buffer.Buffer.concat(chunks); | ||
} | ||
throw new TypeError(`Unable to resolve body.`); | ||
} | ||
__name(resolveBody, "resolveBody"); | ||
function shouldRetry(error) { | ||
@@ -586,6 +622,18 @@ if (error.name === "AbortError") | ||
__name(onRateLimit, "onRateLimit"); | ||
function calculateUserDefaultAvatarIndex(userId) { | ||
return Number(BigInt(userId) >> 22n) % 6; | ||
} | ||
__name(calculateUserDefaultAvatarIndex, "calculateUserDefaultAvatarIndex"); | ||
async function sleep(ms) { | ||
return new Promise((resolve) => { | ||
setTimeout(() => resolve(), ms); | ||
}); | ||
} | ||
__name(sleep, "sleep"); | ||
function isBufferLike(value) { | ||
return value instanceof ArrayBuffer || value instanceof Uint8Array || value instanceof Uint8ClampedArray; | ||
} | ||
__name(isBufferLike, "isBufferLike"); | ||
// src/lib/handlers/Shared.ts | ||
var import_node_timers = require("timers"); | ||
var import_undici3 = require("undici"); | ||
var invalidCount = 0; | ||
@@ -610,13 +658,12 @@ var invalidCountResetTime = null; | ||
const controller = new AbortController(); | ||
const timeout = (0, import_node_timers.setTimeout)(() => controller.abort(), manager.options.timeout).unref(); | ||
const timeout = setTimeout(() => controller.abort(), manager.options.timeout); | ||
if (requestData.signal) { | ||
const signal = requestData.signal; | ||
if (signal.aborted) | ||
if (requestData.signal.aborted) | ||
controller.abort(); | ||
else | ||
signal.addEventListener("abort", () => controller.abort()); | ||
requestData.signal.addEventListener("abort", () => controller.abort()); | ||
} | ||
let res; | ||
try { | ||
res = await (0, import_undici3.request)(url, { ...options, signal: controller.signal }); | ||
res = await manager.options.makeRequest(url, { ...options, signal: controller.signal }); | ||
} catch (error) { | ||
@@ -630,3 +677,3 @@ if (!(error instanceof Error)) | ||
} finally { | ||
(0, import_node_timers.clearTimeout)(timeout); | ||
clearTimeout(timeout); | ||
} | ||
@@ -644,3 +691,3 @@ if (manager.listenerCount("response" /* Response */)) { | ||
}, | ||
{ ...res } | ||
res instanceof Response ? res.clone() : { ...res } | ||
); | ||
@@ -652,3 +699,3 @@ } | ||
async function handleErrors(manager, res, method, url, requestData, retries) { | ||
const status = res.statusCode; | ||
const status = res.status; | ||
if (status >= 500 && status < 600) { | ||
@@ -658,3 +705,3 @@ if (retries !== manager.options.retries) { | ||
} | ||
throw new HTTPError(status, method, url, requestData); | ||
throw new HTTPError(status, res.statusText, method, url, requestData); | ||
} else { | ||
@@ -686,2 +733,5 @@ if (status >= 400 && status < 500) { | ||
} | ||
static { | ||
__name(this, "BurstHandler"); | ||
} | ||
/** | ||
@@ -724,5 +774,5 @@ * {@inheritdoc IHandler.id} | ||
} | ||
const status = res.statusCode; | ||
const status = res.status; | ||
let retryAfter = 0; | ||
const retry = parseHeader(res.headers["retry-after"]); | ||
const retry = res.headers.get("Retry-After"); | ||
if (retry) | ||
@@ -736,3 +786,3 @@ retryAfter = Number(retry) * 1e3 + this.manager.options.offset; | ||
} else if (status === 429) { | ||
const isGlobal = res.headers["x-ratelimit-global"] !== void 0; | ||
const isGlobal = res.headers.has("X-RateLimit-Global"); | ||
await onRateLimit(this.manager, { | ||
@@ -762,3 +812,3 @@ timeToReset: retryAfter, | ||
); | ||
await (0, import_promises.setTimeout)(retryAfter); | ||
await sleep(retryAfter); | ||
return this.runRequest(routeId, url, options, requestData, retries); | ||
@@ -774,6 +824,4 @@ } else { | ||
}; | ||
__name(BurstHandler, "BurstHandler"); | ||
// src/lib/handlers/SequentialHandler.ts | ||
var import_promises2 = require("timers/promises"); | ||
var import_async_queue = require("@sapphire/async-queue"); | ||
@@ -792,2 +840,5 @@ var SequentialHandler = class { | ||
} | ||
static { | ||
__name(this, "SequentialHandler"); | ||
} | ||
/** | ||
@@ -869,3 +920,3 @@ * {@inheritDoc IHandler.id} | ||
async globalDelayFor(time) { | ||
await (0, import_promises2.setTimeout)(time); | ||
await sleep(time); | ||
this.manager.globalDelay = null; | ||
@@ -933,3 +984,3 @@ } | ||
timeout = this.timeToReset; | ||
delay = (0, import_promises2.setTimeout)(timeout); | ||
delay = sleep(timeout); | ||
} | ||
@@ -965,9 +1016,9 @@ const rateLimitData = { | ||
} | ||
const status = res.statusCode; | ||
const status = res.status; | ||
let retryAfter = 0; | ||
const limit = parseHeader(res.headers["x-ratelimit-limit"]); | ||
const remaining = parseHeader(res.headers["x-ratelimit-remaining"]); | ||
const reset = parseHeader(res.headers["x-ratelimit-reset-after"]); | ||
const hash = parseHeader(res.headers["x-ratelimit-bucket"]); | ||
const retry = parseHeader(res.headers["retry-after"]); | ||
const limit = res.headers.get("X-RateLimit-Limit"); | ||
const remaining = res.headers.get("X-RateLimit-Remaining"); | ||
const reset = res.headers.get("X-RateLimit-Reset-After"); | ||
const hash = res.headers.get("X-RateLimit-Bucket"); | ||
const retry = res.headers.get("Retry-After"); | ||
this.limit = limit ? Number(limit) : Number.POSITIVE_INFINITY; | ||
@@ -989,3 +1040,3 @@ this.remaining = remaining ? Number(remaining) : 1; | ||
if (retryAfter > 0) { | ||
if (res.headers["x-ratelimit-global"] !== void 0) { | ||
if (res.headers.has("X-RateLimit-Global")) { | ||
this.manager.globalRemaining = 0; | ||
@@ -1000,3 +1051,3 @@ this.manager.globalReset = Date.now() + retryAfter; | ||
} | ||
if (status >= 200 && status < 300) { | ||
if (res.ok) { | ||
return res; | ||
@@ -1047,3 +1098,3 @@ } else if (status === 429) { | ||
this.#sublimitPromise = null; | ||
await (0, import_promises2.setTimeout)(sublimitTimeout); | ||
await sleep(sublimitTimeout); | ||
let resolve; | ||
@@ -1067,15 +1118,8 @@ const promise = new Promise((res2) => resolve = res2); | ||
}; | ||
__name(SequentialHandler, "SequentialHandler"); | ||
// src/lib/RequestManager.ts | ||
var getFileType = (0, import_util.lazy)(async () => import("file-type")); | ||
var RequestMethod = /* @__PURE__ */ ((RequestMethod2) => { | ||
RequestMethod2["Delete"] = "DELETE"; | ||
RequestMethod2["Get"] = "GET"; | ||
RequestMethod2["Patch"] = "PATCH"; | ||
RequestMethod2["Post"] = "POST"; | ||
RequestMethod2["Put"] = "PUT"; | ||
return RequestMethod2; | ||
})(RequestMethod || {}); | ||
var RequestManager = class extends import_node_events.EventEmitter { | ||
// src/lib/REST.ts | ||
var REST = class _REST extends import_async_event_emitter.AsyncEventEmitter { | ||
static { | ||
__name(this, "REST"); | ||
} | ||
/** | ||
@@ -1086,2 +1130,3 @@ * The {@link https://undici.nodejs.org/#/docs/api/Agent | Agent} for all requests | ||
agent = null; | ||
cdn; | ||
/** | ||
@@ -1111,7 +1156,8 @@ * The number of requests remaining in the global bucket | ||
options; | ||
constructor(options) { | ||
constructor(options = {}) { | ||
super(); | ||
this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn); | ||
this.options = { ...DefaultRestOptions, ...options }; | ||
this.options.offset = Math.max(0, this.options.offset); | ||
this.globalRemaining = this.options.globalRequestsPerSecond; | ||
this.globalRemaining = Math.max(1, this.options.globalRequestsPerSecond); | ||
this.agent = options.agent ?? null; | ||
@@ -1128,3 +1174,3 @@ this.setupSweepers(); | ||
validateMaxInterval(this.options.hashSweepInterval); | ||
this.hashTimer = (0, import_node_timers2.setInterval)(() => { | ||
this.hashTimer = setInterval(() => { | ||
const sweptHashes = new import_collection.Collection(); | ||
@@ -1138,12 +1184,13 @@ const currentDate = Date.now(); | ||
sweptHashes.set(key, val); | ||
this.emit("restDebug" /* Debug */, `Hash ${val.value} for ${key} swept due to lifetime being exceeded`); | ||
} | ||
this.emit("restDebug" /* Debug */, `Hash ${val.value} for ${key} swept due to lifetime being exceeded`); | ||
return shouldSweep; | ||
}); | ||
this.emit("hashSweep" /* HashSweep */, sweptHashes); | ||
}, this.options.hashSweepInterval).unref(); | ||
}, this.options.hashSweepInterval); | ||
this.hashTimer.unref?.(); | ||
} | ||
if (this.options.handlerSweepInterval !== 0 && this.options.handlerSweepInterval !== Number.POSITIVE_INFINITY) { | ||
validateMaxInterval(this.options.handlerSweepInterval); | ||
this.handlerTimer = (0, import_node_timers2.setInterval)(() => { | ||
this.handlerTimer = setInterval(() => { | ||
const sweptHandlers = new import_collection.Collection(); | ||
@@ -1154,11 +1201,66 @@ this.handlers.sweep((val, key) => { | ||
sweptHandlers.set(key, val); | ||
this.emit("restDebug" /* Debug */, `Handler ${val.id} for ${key} swept due to being inactive`); | ||
} | ||
this.emit("restDebug" /* Debug */, `Handler ${val.id} for ${key} swept due to being inactive`); | ||
return inactive; | ||
}); | ||
this.emit("handlerSweep" /* HandlerSweep */, sweptHandlers); | ||
}, this.options.handlerSweepInterval).unref(); | ||
}, this.options.handlerSweepInterval); | ||
this.handlerTimer.unref?.(); | ||
} | ||
} | ||
/** | ||
* Runs a get request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async get(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "GET" /* Get */ }); | ||
} | ||
/** | ||
* Runs a delete request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async delete(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "DELETE" /* Delete */ }); | ||
} | ||
/** | ||
* Runs a post request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async post(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "POST" /* Post */ }); | ||
} | ||
/** | ||
* Runs a put request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async put(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "PUT" /* Put */ }); | ||
} | ||
/** | ||
* Runs a patch request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async patch(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "PATCH" /* Patch */ }); | ||
} | ||
/** | ||
* Runs a request from the api | ||
* | ||
* @param options - Request options | ||
*/ | ||
async request(options) { | ||
const response = await this.queueRequest(options); | ||
return parseResponse(response); | ||
} | ||
/** | ||
* Sets the default agent to use for requests performed by this manager | ||
@@ -1188,3 +1290,3 @@ * | ||
async queueRequest(request2) { | ||
const routeId = RequestManager.generateRouteData(request2.fullRoute, request2.method); | ||
const routeId = _REST.generateRouteData(request2.fullRoute, request2.method); | ||
const hash = this.hashes.get(`${request2.method}:${routeId.bucketRoute}`) ?? { | ||
@@ -1246,17 +1348,16 @@ value: `Global(${request2.method}:${routeId.bucketRoute})`, | ||
if (request2.files?.length) { | ||
const formData = new import_undici4.FormData(); | ||
const formData = new FormData(); | ||
for (const [index, file] of request2.files.entries()) { | ||
const fileKey = file.key ?? `files[${index}]`; | ||
if (import_node_buffer2.Buffer.isBuffer(file.data)) { | ||
const { fileTypeFromBuffer } = await getFileType(); | ||
if (isBufferLike(file.data)) { | ||
let contentType = file.contentType; | ||
if (!contentType) { | ||
const parsedType = (await fileTypeFromBuffer(file.data))?.mime; | ||
const [parsedType] = (0, import_magic_bytes.filetypeinfo)(file.data); | ||
if (parsedType) { | ||
contentType = OverwrittenMimeTypes[parsedType] ?? parsedType; | ||
contentType = OverwrittenMimeTypes[parsedType.mime] ?? parsedType.mime ?? "application/octet-stream"; | ||
} | ||
} | ||
formData.append(fileKey, new import_node_buffer2.Blob([file.data], { type: contentType }), file.name); | ||
formData.append(fileKey, new Blob([file.data], { type: contentType }), file.name); | ||
} else { | ||
formData.append(fileKey, new import_node_buffer2.Blob([`${file.data}`], { type: file.contentType }), file.name); | ||
formData.append(fileKey, new Blob([`${file.data}`], { type: file.contentType }), file.name); | ||
} | ||
@@ -1282,11 +1383,11 @@ } | ||
} | ||
finalBody = await resolveBody(finalBody); | ||
const method = request2.method.toUpperCase(); | ||
const fetchOptions = { | ||
// Set body to null on get / head requests. This does not follow fetch spec (likely because it causes subtle bugs) but is aligned with what request was doing | ||
body: ["GET", "HEAD"].includes(method) ? null : finalBody, | ||
headers: { ...request2.headers, ...additionalHeaders, ...headers }, | ||
method: request2.method.toUpperCase() | ||
method, | ||
// Prioritize setting an agent per request, use the agent for this instance otherwise. | ||
dispatcher: request2.dispatcher ?? this.agent ?? void 0 | ||
}; | ||
if (finalBody !== void 0) { | ||
fetchOptions.body = finalBody; | ||
} | ||
fetchOptions.dispatcher = request2.dispatcher ?? this.agent ?? void 0; | ||
return { url, fetchOptions }; | ||
@@ -1298,3 +1399,3 @@ } | ||
clearHashSweeper() { | ||
(0, import_node_timers2.clearInterval)(this.hashTimer); | ||
clearInterval(this.hashTimer); | ||
} | ||
@@ -1305,3 +1406,3 @@ /** | ||
clearHandlerSweeper() { | ||
(0, import_node_timers2.clearInterval)(this.handlerTimer); | ||
clearInterval(this.handlerTimer); | ||
} | ||
@@ -1341,113 +1442,10 @@ /** | ||
}; | ||
__name(RequestManager, "RequestManager"); | ||
// src/lib/REST.ts | ||
var import_node_events2 = require("events"); | ||
var REST = class extends import_node_events2.EventEmitter { | ||
cdn; | ||
requestManager; | ||
constructor(options = {}) { | ||
super(); | ||
this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn); | ||
this.requestManager = new RequestManager(options).on("restDebug" /* Debug */, this.emit.bind(this, "restDebug" /* Debug */)).on("rateLimited" /* RateLimited */, this.emit.bind(this, "rateLimited" /* RateLimited */)).on("invalidRequestWarning" /* InvalidRequestWarning */, this.emit.bind(this, "invalidRequestWarning" /* InvalidRequestWarning */)).on("hashSweep" /* HashSweep */, this.emit.bind(this, "hashSweep" /* HashSweep */)); | ||
this.on("newListener", (name, listener) => { | ||
if (name === "response" /* Response */) | ||
this.requestManager.on(name, listener); | ||
}); | ||
this.on("removeListener", (name, listener) => { | ||
if (name === "response" /* Response */) | ||
this.requestManager.off(name, listener); | ||
}); | ||
} | ||
/** | ||
* Gets the agent set for this instance | ||
*/ | ||
getAgent() { | ||
return this.requestManager.agent; | ||
} | ||
/** | ||
* Sets the default agent to use for requests performed by this instance | ||
* | ||
* @param agent - Sets the agent to use | ||
*/ | ||
setAgent(agent) { | ||
this.requestManager.setAgent(agent); | ||
return this; | ||
} | ||
/** | ||
* Sets the authorization token that should be used for requests | ||
* | ||
* @param token - The authorization token to use | ||
*/ | ||
setToken(token) { | ||
this.requestManager.setToken(token); | ||
return this; | ||
} | ||
/** | ||
* Runs a get request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async get(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "GET" /* Get */ }); | ||
} | ||
/** | ||
* Runs a delete request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async delete(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "DELETE" /* Delete */ }); | ||
} | ||
/** | ||
* Runs a post request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async post(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "POST" /* Post */ }); | ||
} | ||
/** | ||
* Runs a put request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async put(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "PUT" /* Put */ }); | ||
} | ||
/** | ||
* Runs a patch request from the api | ||
* | ||
* @param fullRoute - The full route to query | ||
* @param options - Optional request options | ||
*/ | ||
async patch(fullRoute, options = {}) { | ||
return this.request({ ...options, fullRoute, method: "PATCH" /* Patch */ }); | ||
} | ||
/** | ||
* Runs a request from the api | ||
* | ||
* @param options - Request options | ||
*/ | ||
async request(options) { | ||
const response = await this.raw(options); | ||
return parseResponse(response); | ||
} | ||
/** | ||
* Runs a request from the API, yielding the raw Response object | ||
* | ||
* @param options - Request options | ||
*/ | ||
async raw(options) { | ||
return this.requestManager.queueRequest(options); | ||
} | ||
}; | ||
__name(REST, "REST"); | ||
// src/shared.ts | ||
var version = "2.0.0"; | ||
// src/index.ts | ||
var version = "1.7.1"; | ||
globalThis.FormData ??= import_undici2.FormData; | ||
globalThis.Blob ??= import_node_buffer.Blob; | ||
setDefaultStrategy((0, import_util2.shouldUseGlobalFetchAndWebSocket)() ? fetch : makeRequest); | ||
// Annotate the CommonJS export names for ESM import in node: | ||
@@ -1469,4 +1467,4 @@ 0 && (module.exports = { | ||
RateLimitError, | ||
RequestManager, | ||
RequestMethod, | ||
calculateUserDefaultAvatarIndex, | ||
makeURLSearchParams, | ||
@@ -1473,0 +1471,0 @@ parseResponse, |
{ | ||
"name": "@discordjs/rest", | ||
"version": "1.7.1", | ||
"version": "2.0.0", | ||
"description": "The REST API for discord.js", | ||
@@ -17,9 +17,21 @@ "scripts": { | ||
}, | ||
"main": "./dist/index.js", | ||
"module": "./dist/index.mjs", | ||
"typings": "./dist/index.d.ts", | ||
"types": "./dist/index.d.ts", | ||
"exports": { | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.mjs", | ||
"require": "./dist/index.js" | ||
".": { | ||
"node": { | ||
"types": "./dist/index.d.ts", | ||
"import": "./dist/index.mjs", | ||
"require": "./dist/index.js" | ||
}, | ||
"default": { | ||
"types": "./dist/web.d.ts", | ||
"import": "./dist/web.mjs", | ||
"require": "./dist/web.js" | ||
} | ||
}, | ||
"./*": { | ||
"types": "./dist/strategies/*.d.ts", | ||
"import": "./dist/strategies/*.mjs", | ||
"require": "./dist/strategies/*.js" | ||
} | ||
}, | ||
@@ -58,25 +70,27 @@ "directories": { | ||
"dependencies": { | ||
"@discordjs/collection": "^1.5.1", | ||
"@discordjs/util": "^0.3.0", | ||
"@discordjs/collection": "^1.5.2", | ||
"@discordjs/util": "^1.0.0", | ||
"@sapphire/async-queue": "^1.5.0", | ||
"@sapphire/snowflake": "^3.4.2", | ||
"discord-api-types": "^0.37.41", | ||
"file-type": "^18.3.0", | ||
"tslib": "^2.5.0", | ||
"undici": "^5.22.0" | ||
"@sapphire/snowflake": "^3.5.1", | ||
"@vladfrangu/async_event_emitter": "^2.2.2", | ||
"discord-api-types": "^0.37.50", | ||
"magic-bytes.js": "^1.0.15", | ||
"tslib": "^2.6.1", | ||
"undici": "^5.22.1" | ||
}, | ||
"devDependencies": { | ||
"@favware/cliff-jumper": "^2.0.0", | ||
"@microsoft/api-extractor": "^7.34.6", | ||
"@types/node": "16.18.25", | ||
"@vitest/coverage-c8": "^0.30.1", | ||
"@favware/cliff-jumper": "^2.1.1", | ||
"@microsoft/api-extractor": "^7.36.3", | ||
"@types/node": "18.17.1", | ||
"@vitest/coverage-c8": "^0.33.0", | ||
"cross-env": "^7.0.3", | ||
"esbuild-plugin-version-injector": "^1.1.0", | ||
"eslint": "^8.39.0", | ||
"eslint-config-neon": "^0.1.42", | ||
"esbuild-plugin-version-injector": "^1.2.0", | ||
"eslint": "^8.46.0", | ||
"eslint-config-neon": "^0.1.47", | ||
"eslint-formatter-pretty": "^5.0.0", | ||
"prettier": "^2.8.8", | ||
"tsup": "^6.7.0", | ||
"typescript": "^5.0.4", | ||
"vitest": "^0.29.8" | ||
"tsup": "^7.1.0", | ||
"turbo": "^1.10.12", | ||
"typescript": "^5.1.6", | ||
"vitest": "^0.33.0" | ||
}, | ||
@@ -83,0 +97,0 @@ "engines": { |
@@ -16,2 +16,3 @@ <div align="center"> | ||
<a href="https://vercel.com/?utm_source=discordjs&utm_campaign=oss"><img src="https://raw.githubusercontent.com/discordjs/discord.js/main/.github/powered-by-vercel.svg" alt="Vercel" /></a> | ||
<a href="https://www.cloudflare.com"><img src="https://raw.githubusercontent.com/discordjs/discord.js/main/.github/powered-by-workers.png" alt="Cloudflare Workers" height="44" /></a> | ||
</p> | ||
@@ -28,2 +29,4 @@ </div> | ||
Note: native fetch (not recommended) is unavailable in this node version, either use a newer node version or use the more performant `undiciRequest` strategy (default) | ||
```sh | ||
@@ -84,2 +87,21 @@ npm install @discordjs/rest | ||
Send a basic message in an edge environment: | ||
```js | ||
import { REST } from '@discordjs/rest'; | ||
import { Routes } from 'discord-api-types/v10'; | ||
const rest = new REST({ version: '10', makeRequest: fetch }).setToken(TOKEN); | ||
try { | ||
await rest.post(Routes.channelMessages(CHANNEL_ID), { | ||
body: { | ||
content: 'A message via REST from the edge!', | ||
}, | ||
}); | ||
} catch (error) { | ||
console.error(error); | ||
} | ||
``` | ||
## Links | ||
@@ -86,0 +108,0 @@ |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
687653
23
6447
138
9
14
6
+ Addedmagic-bytes.js@^1.0.15
+ Added@discordjs/util@1.1.1(transitive)
+ Added@vladfrangu/async_event_emitter@2.4.6(transitive)
+ Addedmagic-bytes.js@1.10.0(transitive)
- Removedfile-type@^18.3.0
- Removed@discordjs/util@0.3.1(transitive)
- Removed@tokenizer/token@0.3.0(transitive)
- Removedfile-type@18.7.0(transitive)
- Removedieee754@1.2.1(transitive)
- Removedinherits@2.0.4(transitive)
- Removedpeek-readable@5.2.0(transitive)
- Removedreadable-stream@3.6.2(transitive)
- Removedreadable-web-to-node-stream@3.0.2(transitive)
- Removedsafe-buffer@5.2.1(transitive)
- Removedstring_decoder@1.3.0(transitive)
- Removedstrtok3@7.1.1(transitive)
- Removedtoken-types@5.0.1(transitive)
- Removedutil-deprecate@1.0.2(transitive)
Updated@discordjs/collection@^1.5.2
Updated@discordjs/util@^1.0.0
Updated@sapphire/snowflake@^3.5.1
Updateddiscord-api-types@^0.37.50
Updatedtslib@^2.6.1
Updatedundici@^5.22.1