UNPKG

@skybloxsystems/ticket-bot

Version:
4 lines 66.5 kB
{ "version": 3, "sources": ["../src/lib/utils/constants.ts", "../src/lib/CDN.ts", "../src/lib/errors/DiscordAPIError.ts", "../src/lib/errors/HTTPError.ts", "../src/lib/errors/RateLimitError.ts", "../src/lib/RequestManager.ts", "../src/lib/handlers/SequentialHandler.ts", "../src/lib/utils/utils.ts", "../src/lib/REST.ts"], "sourcesContent": ["import { APIVersion } from 'discord-api-types/v9';\nimport type { RESTOptions } from '../REST';\n// eslint-disable-next-line @typescript-eslint/no-var-requires, @typescript-eslint/no-require-imports\nconst Package = require('../../../package.json');\n\n// eslint-disable-next-line @typescript-eslint/restrict-template-expressions\nexport const DefaultUserAgent = `DiscordBot (${Package.homepage}, ${Package.version})`;\n\nexport const DefaultRestOptions: Required<RESTOptions> = {\n\tagent: {},\n\tapi: 'https://discord.com/api',\n\tcdn: 'https://cdn.discordapp.com',\n\theaders: {},\n\tinvalidRequestWarningInterval: 0,\n\tglobalRequestsPerSecond: 50,\n\toffset: 50,\n\trejectOnRateLimit: null,\n\tretries: 3,\n\ttimeout: 15_000,\n\tuserAgentAppendix: `Node.js ${process.version}`,\n\tversion: APIVersion,\n};\n\n/**\n * The events that the REST manager emits\n */\nexport const enum RESTEvents {\n\tDebug = 'restDebug',\n\tInvalidRequestWarning = 'invalidRequestWarning',\n\tRateLimited = 'rateLimited',\n\tRequest = 'request',\n\tResponse = 'response',\n}\n\nexport const ALLOWED_EXTENSIONS = ['webp', 'png', 'jpg', 'jpeg', 'gif'] as const;\nexport const ALLOWED_STICKER_EXTENSIONS = ['png', 'json'] as const;\nexport const ALLOWED_SIZES = [16, 32, 64, 128, 256, 512, 1024, 2048, 4096] as const;\n\nexport type ImageExtension = typeof ALLOWED_EXTENSIONS[number];\nexport type StickerExtension = typeof ALLOWED_STICKER_EXTENSIONS[number];\nexport type ImageSize = typeof ALLOWED_SIZES[number];\n", "import {\n\tALLOWED_EXTENSIONS,\n\tALLOWED_SIZES,\n\tALLOWED_STICKER_EXTENSIONS,\n\tDefaultRestOptions,\n\tImageExtension,\n\tImageSize,\n\tStickerExtension,\n} from './utils/constants';\n\nexport interface BaseImageURLOptions {\n\textension?: ImageExtension;\n\tsize?: ImageSize;\n}\n\nexport interface ImageURLOptions extends BaseImageURLOptions {\n\tdynamic?: boolean;\n}\n\nexport interface MakeURLOptions {\n\textension?: string | undefined;\n\tsize?: ImageSize;\n\tallowedExtensions?: readonly string[];\n}\n\n/**\n * The CDN link builder\n */\nexport class CDN {\n\tpublic constructor(private readonly base: string = DefaultRestOptions.cdn) {}\n\n\t/**\n\t * Generates an app asset URL for a client's asset.\n\t * @param clientId The client id that has the asset\n\t * @param assetHash The hash provided by Discord for this asset\n\t * @param options Optional options for the asset\n\t */\n\tpublic appAsset(clientId: string, assetHash: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/app-assets/${clientId}/${assetHash}`, options);\n\t}\n\n\t/**\n\t * Generates an app icon URL for a client's icon.\n\t * @param clientId The client id that has the icon\n\t * @param iconHash The hash provided by Discord for this icon\n\t * @param options Optional options for the icon\n\t */\n\tpublic appIcon(clientId: string, iconHash: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/app-icons/${clientId}/${iconHash}`, options);\n\t}\n\n\t/**\n\t * Generates an avatar URL, e.g. for a user or a webhook.\n\t * @param id The id that has the icon\n\t * @param avatarHash The hash provided by Discord for this avatar\n\t * @param options Optional options for the avatar\n\t */\n\tpublic avatar(id: string, avatarHash: string, options?: Readonly<ImageURLOptions>): string {\n\t\treturn this.dynamicMakeURL(`/avatars/${id}/${avatarHash}`, avatarHash, options);\n\t}\n\n\t/**\n\t * Generates a banner URL, e.g. for a user or a guild.\n\t * @param id The id that has the banner splash\n\t * @param bannerHash The hash provided by Discord for this banner\n\t * @param options Optional options for the banner\n\t */\n\tpublic banner(id: string, bannerHash: string, options?: Readonly<ImageURLOptions>): string {\n\t\treturn this.dynamicMakeURL(`/banners/${id}/${bannerHash}`, bannerHash, options);\n\t}\n\n\t/**\n\t * Generates an icon URL for a channel, e.g. a group DM.\n\t * @param channelId The channel id that has the icon\n\t * @param iconHash The hash provided by Discord for this channel\n\t * @param options Optional options for the icon\n\t */\n\tpublic channelIcon(channelId: string, iconHash: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/channel-icons/${channelId}/${iconHash}`, options);\n\t}\n\n\t/**\n\t * Generates the default avatar URL for a discriminator.\n\t * @param discriminator The discriminator modulo 5\n\t */\n\tpublic defaultAvatar(discriminator: number): string {\n\t\treturn this.makeURL(`/embed/avatars/${discriminator}`);\n\t}\n\n\t/**\n\t * Generates a discovery splash URL for a guild's discovery splash.\n\t * @param guildId The guild id that has the discovery splash\n\t * @param splashHash The hash provided by Discord for this splash\n\t * @param options Optional options for the splash\n\t */\n\tpublic discoverySplash(guildId: string, splashHash: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/discovery-splashes/${guildId}/${splashHash}`, options);\n\t}\n\n\t/**\n\t * Generates an emoji's URL for an emoji.\n\t * @param emojiId The emoji id\n\t * @param extension The extension of the emoji\n\t */\n\tpublic emoji(emojiId: string, extension?: ImageExtension): string {\n\t\treturn this.makeURL(`/emojis/${emojiId}`, { extension });\n\t}\n\n\t/**\n\t * Generates a guild member avatar URL.\n\t * @param guildId The id of the guild\n\t * @param userId The id of the user\n\t * @param avatarHash The hash provided by Discord for this avatar\n\t * @param options Optional options for the avatar\n\t */\n\tpublic guildMemberAvatar(\n\t\tguildId: string,\n\t\tuserId: string,\n\t\tavatarHash: string,\n\t\toptions?: Readonly<ImageURLOptions>,\n\t): string {\n\t\treturn this.dynamicMakeURL(`/guilds/${guildId}/users/${userId}/avatars/${avatarHash}`, avatarHash, options);\n\t}\n\n\t/**\n\t * Generates an icon URL, e.g. for a guild.\n\t * @param id The id that has the icon splash\n\t * @param iconHash The hash provided by Discord for this icon\n\t * @param options Optional options for the icon\n\t */\n\tpublic icon(id: string, iconHash: string, options?: Readonly<ImageURLOptions>): string {\n\t\treturn this.dynamicMakeURL(`/icons/${id}/${iconHash}`, iconHash, options);\n\t}\n\n\t/**\n\t * Generates a URL for the icon of a role\n\t * @param roleId The id of the role that has the icon\n\t * @param roleIconHash The hash provided by Discord for this role icon\n\t * @param options Optional options for the role icon\n\t */\n\tpublic roleIcon(roleId: string, roleIconHash: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/role-icons/${roleId}/${roleIconHash}`, options);\n\t}\n\n\t/**\n\t * Generates a guild invite splash URL for a guild's invite splash.\n\t * @param guildId The guild id that has the invite splash\n\t * @param splashHash The hash provided by Discord for this splash\n\t * @param options Optional options for the splash\n\t */\n\tpublic splash(guildId: string, splashHash: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/splashes/${guildId}/${splashHash}`, options);\n\t}\n\n\t/**\n\t * Generates a sticker URL.\n\t * @param stickerId The sticker id\n\t * @param extension The extension of the sticker\n\t */\n\tpublic sticker(stickerId: string, extension?: StickerExtension): string {\n\t\treturn this.makeURL(`/stickers/${stickerId}`, { allowedExtensions: ALLOWED_STICKER_EXTENSIONS, extension });\n\t}\n\n\t/**\n\t * Generates a sticker pack banner URL.\n\t * @param bannerId The banner id\n\t * @param options Optional options for the banner\n\t */\n\tpublic stickerPackBanner(bannerId: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/app-assets/710982414301790216/store/${bannerId}`, options);\n\t}\n\n\t/**\n\t * Generates a team icon URL for a team's icon.\n\t * @param teamId The team id that has the icon\n\t * @param iconHash The hash provided by Discord for this icon\n\t * @param options Optional options for the icon\n\t */\n\tpublic teamIcon(teamId: string, iconHash: string, options?: Readonly<BaseImageURLOptions>): string {\n\t\treturn this.makeURL(`/team-icons/${teamId}/${iconHash}`, options);\n\t}\n\n\t/**\n\t * Constructs the URL for the resource, checking whether or not `hash` starts with `a_` if `dynamic` is set to `true`.\n\t * @param route The base cdn route\n\t * @param hash The hash provided by Discord for this icon\n\t * @param options Optional options for the link\n\t */\n\tprivate dynamicMakeURL(\n\t\troute: string,\n\t\thash: string,\n\t\t{ dynamic = false, ...options }: Readonly<ImageURLOptions> = {},\n\t): string {\n\t\treturn this.makeURL(route, dynamic && hash.startsWith('a_') ? { ...options, extension: 'gif' } : options);\n\t}\n\n\t/**\n\t * Constructs the URL for the resource\n\t * @param route The base cdn route\n\t * @param options The extension/size options for the link\n\t */\n\tprivate makeURL(\n\t\troute: string,\n\t\t{ allowedExtensions = ALLOWED_EXTENSIONS, extension = 'png', size }: Readonly<MakeURLOptions> = {},\n\t): string {\n\t\textension = String(extension).toLowerCase();\n\n\t\tif (!allowedExtensions.includes(extension)) {\n\t\t\tthrow new RangeError(`Invalid extension provided: ${extension}\\nMust be one of: ${allowedExtensions.join(', ')}`);\n\t\t}\n\n\t\tif (size && !ALLOWED_SIZES.includes(size)) {\n\t\t\tthrow new RangeError(`Invalid size provided: ${size}\\nMust be one of: ${ALLOWED_SIZES.join(', ')}`);\n\t\t}\n\n\t\tconst url = new URL(`${this.base}${route}.${extension}`);\n\n\t\tif (size) {\n\t\t\turl.searchParams.set('size', String(size));\n\t\t}\n\n\t\treturn url.toString();\n\t}\n}\n", "import type { InternalRequest, RawAttachment } from '../RequestManager';\n\ninterface DiscordErrorFieldInformation {\n\tcode: string;\n\tmessage: string;\n}\n\ninterface DiscordErrorGroupWrapper {\n\t_errors: DiscordError[];\n}\n\ntype DiscordError = DiscordErrorGroupWrapper | DiscordErrorFieldInformation | { [k: string]: DiscordError } | string;\n\nexport interface DiscordErrorData {\n\tcode: number;\n\tmessage: string;\n\terrors?: DiscordError;\n}\n\nexport interface RequestBody {\n\tattachments: RawAttachment[] | undefined;\n\tjson: unknown | undefined;\n}\n\nfunction isErrorGroupWrapper(error: DiscordError): error is DiscordErrorGroupWrapper {\n\treturn Reflect.has(error as Record<string, unknown>, '_errors');\n}\n\nfunction isErrorResponse(error: DiscordError): error is DiscordErrorFieldInformation {\n\treturn typeof Reflect.get(error as Record<string, unknown>, 'message') === 'string';\n}\n\n/**\n * Represents an API error returned by Discord\n * @extends Error\n */\nexport class DiscordAPIError extends Error {\n\tpublic requestBody: RequestBody;\n\n\t/**\n\t * @param rawError The error reported by Discord\n\t * @param code The error code reported by Discord\n\t * @param status The status code of the response\n\t * @param method The method of the request that erred\n\t * @param url The url of the request that erred\n\t * @param bodyData The unparsed data for the request that errored\n\t */\n\tpublic constructor(\n\t\tpublic rawError: DiscordErrorData,\n\t\tpublic code: number,\n\t\tpublic status: number,\n\t\tpublic method: string,\n\t\tpublic url: string,\n\t\tbodyData: Pick<InternalRequest, 'attachments' | 'body'>,\n\t) {\n\t\tsuper(DiscordAPIError.getMessage(rawError));\n\n\t\tthis.requestBody = { attachments: bodyData.attachments, json: bodyData.body };\n\t}\n\n\t/**\n\t * The name of the error\n\t */\n\tpublic override get name(): string {\n\t\treturn `${DiscordAPIError.name}[${this.code}]`;\n\t}\n\n\tprivate static getMessage(error: DiscordErrorData) {\n\t\tlet flattened = '';\n\t\tif (error.errors) {\n\t\t\tflattened = [...this.flattenDiscordError(error.errors)].join('\\n');\n\t\t}\n\t\treturn error.message && flattened\n\t\t\t? `${error.message}\\n${flattened}`\n\t\t\t: error.message || flattened || 'Unknown Error';\n\t}\n\n\tprivate static *flattenDiscordError(obj: DiscordError, key = ''): IterableIterator<string> {\n\t\tif (isErrorResponse(obj)) {\n\t\t\treturn yield `${key.length ? `${key}[${obj.code}]` : `${obj.code}`}: ${obj.message}`.trim();\n\t\t}\n\n\t\tfor (const [k, v] of Object.entries(obj)) {\n\t\t\tconst nextKey = k.startsWith('_') ? key : key ? (Number.isNaN(Number(k)) ? `${key}.${k}` : `${key}[${k}]`) : k;\n\n\t\t\tif (typeof v === 'string') {\n\t\t\t\tyield v;\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\t\t} else if (isErrorGroupWrapper(v)) {\n\t\t\t\tfor (const error of v._errors) {\n\t\t\t\t\tyield* this.flattenDiscordError(error, nextKey);\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// eslint-disable-next-line @typescript-eslint/no-unsafe-argument\n\t\t\t\tyield* this.flattenDiscordError(v, nextKey);\n\t\t\t}\n\t\t}\n\t}\n}\n", "import type { InternalRequest } from '../RequestManager';\nimport type { RequestBody } from './DiscordAPIError';\n\n/**\n * Represents a HTTP error\n */\nexport class HTTPError extends Error {\n\tpublic requestBody: RequestBody;\n\n\t/**\n\t * @param message The error message\n\t * @param name The name of the error\n\t * @param status The status code of the response\n\t * @param method The method of the request that erred\n\t * @param url The url of the request that erred\n\t * @param bodyData The unparsed data for the request that errored\n\t */\n\tpublic constructor(\n\t\tmessage: string,\n\t\tpublic override name: string,\n\t\tpublic status: number,\n\t\tpublic method: string,\n\t\tpublic url: string,\n\t\tbodyData: Pick<InternalRequest, 'attachments' | 'body'>,\n\t) {\n\t\tsuper(message);\n\n\t\tthis.requestBody = { attachments: bodyData.attachments, json: bodyData.body };\n\t}\n}\n", "import type { RateLimitData } from '../REST';\n\nexport class RateLimitError extends Error implements RateLimitData {\n\tpublic timeToReset: number;\n\tpublic limit: number;\n\tpublic method: string;\n\tpublic hash: string;\n\tpublic url: string;\n\tpublic route: string;\n\tpublic majorParameter: string;\n\tpublic global: boolean;\n\tpublic constructor({ timeToReset, limit, method, hash, url, route, majorParameter, global }: RateLimitData) {\n\t\tsuper();\n\t\tthis.timeToReset = timeToReset;\n\t\tthis.limit = limit;\n\t\tthis.method = method;\n\t\tthis.hash = hash;\n\t\tthis.url = url;\n\t\tthis.route = route;\n\t\tthis.majorParameter = majorParameter;\n\t\tthis.global = global;\n\t}\n\n\t/**\n\t * The name of the error\n\t */\n\tpublic override get name(): string {\n\t\treturn `${RateLimitError.name}[${this.route}]`;\n\t}\n}\n", "import Collection from '@discordjs/collection';\nimport FormData from 'form-data';\nimport { DiscordSnowflake } from '@sapphire/snowflake';\nimport { EventEmitter } from 'node:events';\nimport { Agent } from 'node:https';\nimport type { RequestInit } from 'node-fetch';\nimport type { IHandler } from './handlers/IHandler';\nimport { SequentialHandler } from './handlers/SequentialHandler';\nimport type { RESTOptions, RestEvents } from './REST';\nimport { DefaultRestOptions, DefaultUserAgent } from './utils/constants';\n\nlet agent: Agent | null = null;\n\n/**\n * Represents an attachment to be added to the request\n */\nexport interface RawAttachment {\n\t/**\n\t * The name of the file\n\t */\n\tfileName: string;\n\t/**\n\t * An explicit key to use for key of the formdata field for this attachment.\n\t * When not provided, the index of the file in the attachments array is used in the form `files[${index}]`.\n\t * If you wish to alter the placeholder snowflake, you must provide this property in the same form (`files[${placeholder}]`)\n\t */\n\tkey?: string;\n\t/**\n\t * The actual data for the attachment\n\t */\n\trawBuffer: Buffer;\n}\n\n/**\n * Represents possible data to be given to an endpoint\n */\nexport interface RequestData {\n\t/**\n\t * Whether to append JSON data to form data instead of `payload_json` when sending attachments\n\t */\n\tappendToFormData?: boolean;\n\t/**\n\t * Files to be attached to this request\n\t */\n\tattachments?: RawAttachment[] | undefined;\n\t/**\n\t * If this request needs the `Authorization` header\n\t * @default true\n\t */\n\tauth?: boolean;\n\t/**\n\t * The authorization prefix to use for this request, useful if you use this with bearer tokens\n\t * @default 'Bot'\n\t */\n\tauthPrefix?: 'Bot' | 'Bearer';\n\t/**\n\t * The body to send to this request\n\t */\n\tbody?: unknown;\n\t/**\n\t * Additional headers to add to this request\n\t */\n\theaders?: Record<string, string>;\n\t/**\n\t * Query string parameters to append to the called endpoint\n\t */\n\tquery?: URLSearchParams;\n\t/**\n\t * Reason to show in the audit logs\n\t */\n\treason?: string;\n\t/**\n\t * If this request should be versioned\n\t * @default true\n\t */\n\tversioned?: boolean;\n}\n\n/**\n * Possible headers for an API call\n */\nexport interface RequestHeaders {\n\tAuthorization?: string;\n\t'User-Agent': string;\n\t'X-Audit-Log-Reason'?: string;\n}\n\n/**\n * Possible API methods to be used when doing requests\n */\nexport const enum RequestMethod {\n\tDelete = 'delete',\n\tGet = 'get',\n\tPatch = 'patch',\n\tPost = 'post',\n\tPut = 'put',\n}\n\nexport type RouteLike = `/${string}`;\n\n/**\n * Internal request options\n *\n * @internal\n */\nexport interface InternalRequest extends RequestData {\n\tmethod: RequestMethod;\n\tfullRoute: RouteLike;\n}\n\n/**\n * Parsed route data for an endpoint\n *\n * @internal\n */\nexport interface RouteData {\n\tmajorParameter: string;\n\tbucketRoute: string;\n\toriginal: RouteLike;\n}\n\nexport interface RequestManager {\n\ton<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void): this;\n\ton<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void): this;\n\n\tonce<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void): this;\n\tonce<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void): this;\n\n\temit<K extends keyof RestEvents>(event: K, ...args: RestEvents[K]): boolean;\n\temit<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, ...args: any[]): boolean;\n\n\toff<K extends keyof RestEvents>(event: K, listener: (...args: RestEvents[K]) => void): this;\n\toff<S extends string | symbol>(event: Exclude<S, keyof RestEvents>, listener: (...args: any[]) => void): this;\n\n\tremoveAllListeners<K extends keyof RestEvents>(event?: K): this;\n\tremoveAllListeners<S extends string | symbol>(event?: Exclude<S, keyof RestEvents>): this;\n}\n\n/**\n * Represents the class that manages handlers for endpoints\n */\nexport class RequestManager extends EventEmitter {\n\t/**\n\t * The number of requests remaining in the global bucket\n\t */\n\tpublic globalRemaining: number;\n\n\t/**\n\t * The promise used to wait out the global rate limit\n\t */\n\tpublic globalDelay: Promise<void> | null = null;\n\n\t/**\n\t * The timestamp at which the global bucket resets\n\t */\n\tpublic globalReset = -1;\n\n\t/**\n\t * API bucket hashes that are cached from provided routes\n\t */\n\tpublic readonly hashes = new Collection<string, string>();\n\n\t/**\n\t * Request handlers created from the bucket hash and the major parameters\n\t */\n\tpublic readonly handlers = new Collection<string, IHandler>();\n\n\t// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility\n\t#token: string | null = null;\n\n\tpublic readonly options: RESTOptions;\n\n\tpublic constructor(options: Partial<RESTOptions>) {\n\t\tsuper();\n\t\tthis.options = { ...DefaultRestOptions, ...options };\n\t\tthis.options.offset = Math.max(0, this.options.offset);\n\t\tthis.globalRemaining = this.options.globalRequestsPerSecond;\n\t}\n\n\t/**\n\t * Sets the authorization token that should be used for requests\n\t * @param token The authorization token to use\n\t */\n\tpublic setToken(token: string) {\n\t\tthis.#token = token;\n\t\treturn this;\n\t}\n\n\t/**\n\t * Queues a request to be sent\n\t * @param request All the information needed to make a request\n\t * @returns The response from the api request\n\t */\n\tpublic async queueRequest(request: InternalRequest): Promise<unknown> {\n\t\t// Generalize the endpoint to its route data\n\t\tconst routeId = RequestManager.generateRouteData(request.fullRoute, request.method);\n\t\t// Get the bucket hash for the generic route, or point to a global route otherwise\n\t\tconst hash =\n\t\t\tthis.hashes.get(`${request.method}:${routeId.bucketRoute}`) ?? `Global(${request.method}:${routeId.bucketRoute})`;\n\n\t\t// Get the request handler for the obtained hash, with its major parameter\n\t\tconst handler =\n\t\t\tthis.handlers.get(`${hash}:${routeId.majorParameter}`) ?? this.createHandler(hash, routeId.majorParameter);\n\n\t\t// Resolve the request into usable fetch/node-fetch options\n\t\tconst { url, fetchOptions } = this.resolveRequest(request);\n\n\t\t// Queue the request\n\t\treturn handler.queueRequest(routeId, url, fetchOptions, { body: request.body, attachments: request.attachments });\n\t}\n\n\t/**\n\t * Creates a new rate limit handler from a hash, based on the hash and the major parameter\n\t * @param hash The hash for the route\n\t * @param majorParameter The major parameter for this handler\n\t * @private\n\t */\n\tprivate createHandler(hash: string, majorParameter: string) {\n\t\t// Create the async request queue to handle requests\n\t\tconst queue = new SequentialHandler(this, hash, majorParameter);\n\t\t// Save the queue based on its id\n\t\tthis.handlers.set(queue.id, queue);\n\n\t\treturn queue;\n\t}\n\n\t/**\n\t * Formats the request data to a usable format for fetch\n\t * @param request The request data\n\t */\n\tprivate resolveRequest(request: InternalRequest): { url: string; fetchOptions: RequestInit } {\n\t\tconst { options } = this;\n\n\t\tagent ??= new Agent({ ...options.agent, keepAlive: true });\n\n\t\tlet query = '';\n\n\t\t// If a query option is passed, use it\n\t\tif (request.query) {\n\t\t\tquery = `?${request.query.toString()}`;\n\t\t}\n\n\t\t// Create the required headers\n\t\tconst headers: RequestHeaders = {\n\t\t\t...this.options.headers,\n\t\t\t'User-Agent': `${DefaultUserAgent} ${options.userAgentAppendix}`.trim(),\n\t\t};\n\n\t\t// If this request requires authorization (allowing non-\"authorized\" requests for webhooks)\n\t\tif (request.auth !== false) {\n\t\t\t// If we haven't received a token, throw an error\n\t\t\tif (!this.#token) {\n\t\t\t\tthrow new Error('Expected token to be set for this request, but none was present');\n\t\t\t}\n\n\t\t\theaders.Authorization = `${request.authPrefix ?? 'Bot'} ${this.#token}`;\n\t\t}\n\n\t\t// If a reason was set, set it's appropriate header\n\t\tif (request.reason?.length) {\n\t\t\theaders['X-Audit-Log-Reason'] = encodeURIComponent(request.reason);\n\t\t}\n\n\t\t// Format the full request URL (api base, optional version, endpoint, optional querystring)\n\t\tconst url = `${options.api}${request.versioned === false ? '' : `/v${options.version}`}${\n\t\t\trequest.fullRoute\n\t\t}${query}`;\n\n\t\tlet finalBody: RequestInit['body'];\n\t\tlet additionalHeaders: Record<string, string> = {};\n\n\t\tif (request.attachments?.length) {\n\t\t\tconst formData = new FormData();\n\n\t\t\t// Attach all files to the request\n\t\t\tfor (const [index, attachment] of request.attachments.entries()) {\n\t\t\t\tformData.append(attachment.key ?? `files[${index}]`, attachment.rawBuffer, attachment.fileName);\n\t\t\t}\n\n\t\t\t// If a JSON body was added as well, attach it to the form data, using payload_json unless otherwise specified\n\t\t\t// eslint-disable-next-line no-eq-null\n\t\t\tif (request.body != null) {\n\t\t\t\tif (request.appendToFormData) {\n\t\t\t\t\tfor (const [key, value] of Object.entries(request.body as Record<string, unknown>)) {\n\t\t\t\t\t\tformData.append(key, value);\n\t\t\t\t\t}\n\t\t\t\t} else {\n\t\t\t\t\tformData.append('payload_json', JSON.stringify(request.body));\n\t\t\t\t}\n\t\t\t}\n\n\t\t\t// Set the final body to the form data\n\t\t\tfinalBody = formData;\n\t\t\t// Set the additional headers to the form data ones\n\t\t\tadditionalHeaders = formData.getHeaders();\n\n\t\t\t// eslint-disable-next-line no-eq-null\n\t\t} else if (request.body != null) {\n\t\t\t// Stringify the JSON data\n\t\t\tfinalBody = JSON.stringify(request.body);\n\t\t\t// Set the additional headers to specify the content-type\n\t\t\tadditionalHeaders = { 'Content-Type': 'application/json' };\n\t\t}\n\n\t\tconst fetchOptions = {\n\t\t\tagent,\n\t\t\tbody: finalBody,\n\t\t\t// eslint-disable-next-line @typescript-eslint/consistent-type-assertions\n\t\t\theaders: { ...(request.headers ?? {}), ...additionalHeaders, ...headers } as Record<string, string>,\n\t\t\tmethod: request.method,\n\t\t};\n\n\t\treturn { url, fetchOptions };\n\t}\n\n\t/**\n\t * Generates route data for an endpoint:method\n\t * @param endpoint The raw endpoint to generalize\n\t * @param method The HTTP method this endpoint is called without\n\t * @private\n\t */\n\tprivate static generateRouteData(endpoint: RouteLike, method: RequestMethod): RouteData {\n\t\tconst majorIdMatch = /^\\/(?:channels|guilds|webhooks)\\/(\\d{16,19})/.exec(endpoint);\n\n\t\t// Get the major id for this route - global otherwise\n\t\tconst majorId = majorIdMatch?.[1] ?? 'global';\n\n\t\tconst baseRoute = endpoint\n\t\t\t// Strip out all ids\n\t\t\t.replace(/\\d{16,19}/g, ':id')\n\t\t\t// Strip out reaction as they fall under the same bucket\n\t\t\t.replace(/\\/reactions\\/(.*)/, '/reactions/:reaction');\n\n\t\tlet exceptions = '';\n\n\t\t// Hard-Code Old Message Deletion Exception (2 week+ old messages are a different bucket)\n\t\t// https://github.com/discord/discord-api-docs/issues/1295\n\t\tif (method === RequestMethod.Delete && baseRoute === '/channels/:id/messages/:id') {\n\t\t\tconst id = /\\d{16,19}$/.exec(endpoint)![0];\n\t\t\tconst snowflake = DiscordSnowflake.deconstruct(id);\n\t\t\tif (Date.now() - Number(snowflake.timestamp) > 1000 * 60 * 60 * 24 * 14) {\n\t\t\t\texceptions += '/Delete Old Message';\n\t\t\t}\n\t\t}\n\n\t\treturn {\n\t\t\tmajorParameter: majorId,\n\t\t\tbucketRoute: baseRoute + exceptions,\n\t\t\toriginal: endpoint,\n\t\t};\n\t}\n}\n", "import { setTimeout as sleep } from 'node:timers/promises';\nimport { AsyncQueue } from '@sapphire/async-queue';\nimport fetch, { RequestInit, Response } from 'node-fetch';\nimport { DiscordAPIError, DiscordErrorData } from '../errors/DiscordAPIError';\nimport { HTTPError } from '../errors/HTTPError';\nimport { RateLimitError } from '../errors/RateLimitError';\nimport type { InternalRequest, RequestManager, RouteData } from '../RequestManager';\nimport { RESTEvents } from '../utils/constants';\nimport { hasSublimit, parseResponse } from '../utils/utils';\nimport type { RateLimitData } from '../REST';\n\n/* Invalid request limiting is done on a per-IP basis, not a per-token basis.\n * The best we can do is track invalid counts process-wide (on the theory that\n * users could have multiple bots run from one process) rather than per-bot.\n * Therefore, store these at file scope here rather than in the client's\n * RESTManager object.\n */\nlet invalidCount = 0;\nlet invalidCountResetTime: number | null = null;\n\nconst enum QueueType {\n\tStandard,\n\tSublimit,\n}\n\n/**\n * The structure used to handle requests for a given bucket\n */\nexport class SequentialHandler {\n\t/**\n\t * The unique id of the handler\n\t */\n\tpublic readonly id: string;\n\n\t/**\n\t * The time this rate limit bucket will reset\n\t */\n\tprivate reset = -1;\n\n\t/**\n\t * The remaining requests that can be made before we are rate limited\n\t */\n\tprivate remaining = 1;\n\n\t/**\n\t * The total number of requests that can be made before we are rate limited\n\t */\n\tprivate limit = Infinity;\n\n\t/**\n\t * The interface used to sequence async requests sequentially\n\t */\n\t// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility\n\t#asyncQueue = new AsyncQueue();\n\n\t/**\n\t * The interface used to sequence sublimited async requests sequentially\n\t */\n\t// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility\n\t#sublimitedQueue: AsyncQueue | null = null;\n\n\t/**\n\t * A promise wrapper for when the sublimited queue is finished being processed or null when not being processed\n\t */\n\t// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility\n\t#sublimitPromise: { promise: Promise<void>; resolve: () => void } | null = null;\n\n\t/**\n\t * Whether the sublimit queue needs to be shifted in the finally block\n\t */\n\t// eslint-disable-next-line @typescript-eslint/explicit-member-accessibility\n\t#shiftSublimit = false;\n\n\t/**\n\t * @param manager The request manager\n\t * @param hash The hash that this RequestHandler handles\n\t * @param majorParameter The major parameter for this handler\n\t */\n\tpublic constructor(\n\t\tprivate readonly manager: RequestManager,\n\t\tprivate readonly hash: string,\n\t\tprivate readonly majorParameter: string,\n\t) {\n\t\tthis.id = `${hash}:${majorParameter}`;\n\t}\n\n\t/**\n\t * If the bucket is currently inactive (no pending requests)\n\t */\n\tpublic get inactive(): boolean {\n\t\treturn (\n\t\t\tthis.#asyncQueue.remaining === 0 &&\n\t\t\t(this.#sublimitedQueue === null || this.#sublimitedQueue.remaining === 0) &&\n\t\t\t!this.limited\n\t\t);\n\t}\n\n\t/**\n\t * If the rate limit bucket is currently limited by the global limit\n\t */\n\tprivate get globalLimited(): boolean {\n\t\treturn this.manager.globalRemaining <= 0 && Date.now() < this.manager.globalReset;\n\t}\n\n\t/**\n\t * If the rate limit bucket is currently limited by its limit\n\t */\n\tprivate get localLimited(): boolean {\n\t\treturn this.remaining <= 0 && Date.now() < this.reset;\n\t}\n\n\t/**\n\t * If the rate limit bucket is currently limited\n\t */\n\tprivate get limited(): boolean {\n\t\treturn this.globalLimited || this.localLimited;\n\t}\n\n\t/**\n\t * The time until queued requests can continue\n\t */\n\tprivate get timeToReset(): number {\n\t\treturn this.reset + this.manager.options.offset - Date.now();\n\t}\n\n\t/**\n\t * Emits a debug message\n\t * @param message The message to debug\n\t */\n\tprivate debug(message: string) {\n\t\tthis.manager.emit(RESTEvents.Debug, `[REST ${this.id}] ${message}`);\n\t}\n\n\t/**\n\t * Delay all requests for the specified amount of time, handling global rate limits\n\t * @param time The amount of time to delay all requests for\n\t * @returns\n\t */\n\tprivate async globalDelayFor(time: number): Promise<void> {\n\t\tawait sleep(time, undefined, { ref: false });\n\t\tthis.manager.globalDelay = null;\n\t}\n\n\t/*\n\t * Determines whether the request should be queued or whether a RateLimitError should be thrown\n\t */\n\tprivate async onRateLimit(rateLimitData: RateLimitData) {\n\t\tconst { options } = this.manager;\n\t\tif (!options.rejectOnRateLimit) return;\n\n\t\tconst shouldThrow =\n\t\t\ttypeof options.rejectOnRateLimit === 'function'\n\t\t\t\t? await options.rejectOnRateLimit(rateLimitData)\n\t\t\t\t: options.rejectOnRateLimit.some((route) => rateLimitData.route.startsWith(route.toLowerCase()));\n\t\tif (shouldThrow) {\n\t\t\tthrow new RateLimitError(rateLimitData);\n\t\t}\n\t}\n\n\t/**\n\t * Queues a request to be sent\n\t * @param routeId The generalized api route with literal ids for major parameters\n\t * @param url The url to do the request on\n\t * @param options All the information needed to make a request\n\t * @param bodyData The data that was used to form the body, passed to any errors generated and for determining whether to sublimit\n\t */\n\tpublic async queueRequest(\n\t\trouteId: RouteData,\n\t\turl: string,\n\t\toptions: RequestInit,\n\t\tbodyData: Pick<InternalRequest, 'attachments' | 'body'>,\n\t): Promise<unknown> {\n\t\tlet queue = this.#asyncQueue;\n\t\tlet queueType = QueueType.Standard;\n\t\t// Separate sublimited requests when already sublimited\n\t\tif (this.#sublimitedQueue && hasSublimit(routeId.bucketRoute, bodyData.body, options.method)) {\n\t\t\tqueue = this.#sublimitedQueue!;\n\t\t\tqueueType = QueueType.Sublimit;\n\t\t}\n\t\t// Wait for any previous requests to be completed before this one is run\n\t\tawait queue.wait();\n\t\t// This set handles retroactively sublimiting requests\n\t\tif (queueType === QueueType.Standard) {\n\t\t\tif (this.#sublimitedQueue && hasSublimit(routeId.bucketRoute, bodyData.body, options.method)) {\n\t\t\t\t/**\n\t\t\t\t * Remove the request from the standard queue, it should never be possible to get here while processing the\n\t\t\t\t * sublimit queue so there is no need to worry about shifting the wrong request\n\t\t\t\t */\n\t\t\t\tqueue = this.#sublimitedQueue!;\n\t\t\t\tconst wait = queue.wait();\n\t\t\t\tthis.#asyncQueue.shift();\n\t\t\t\tawait wait;\n\t\t\t} else if (this.#sublimitPromise) {\n\t\t\t\t// Stall requests while the sublimit queue gets processed\n\t\t\t\tawait this.#sublimitPromise.promise;\n\t\t\t}\n\t\t}\n\t\ttry {\n\t\t\t// Make the request, and return the results\n\t\t\treturn await this.runRequest(routeId, url, options, bodyData);\n\t\t} finally {\n\t\t\t// Allow the next request to fire\n\t\t\tqueue.shift();\n\t\t\tif (this.#shiftSublimit) {\n\t\t\t\tthis.#shiftSublimit = false;\n\t\t\t\tthis.#sublimitedQueue?.shift();\n\t\t\t}\n\t\t\t// If this request is the last request in a sublimit\n\t\t\tif (this.#sublimitedQueue?.remaining === 0) {\n\t\t\t\tthis.#sublimitPromise?.resolve();\n\t\t\t\tthis.#sublimitedQueue = null;\n\t\t\t}\n\t\t}\n\t}\n\n\t/**\n\t * The method that actually makes the request to the api, and updates info about the bucket accordingly\n\t * @param routeId The generalized api route with literal ids for major parameters\n\t * @param url The fully resolved url to make the request to\n\t * @param options The node-fetch options needed to make the request\n\t * @param bodyData The data that was used to form the body, passed to any errors generated\n\t * @param retries The number of retries this request has already attempted (recursion)\n\t */\n\tprivate async runRequest(\n\t\trouteId: RouteData,\n\t\turl: string,\n\t\toptions: RequestInit,\n\t\tbodyData: Pick<InternalRequest, 'attachments' | 'body'>,\n\t\tretries = 0,\n\t): Promise<unknown> {\n\t\t/*\n\t\t * After calculations have been done, pre-emptively stop further requests\n\t\t * Potentially loop until this task can run if e.g. the global rate limit is hit twice\n\t\t */\n\t\twhile (this.limited) {\n\t\t\tconst isGlobal = this.globalLimited;\n\t\t\tlet limit: number;\n\t\t\tlet timeout: number;\n\t\t\tlet delay: Promise<void>;\n\n\t\t\tif (isGlobal) {\n\t\t\t\t// Set RateLimitData based on the globl limit\n\t\t\t\tlimit = this.manager.options.globalRequestsPerSecond;\n\t\t\t\ttimeout = this.manager.globalReset + this.manager.options.offset - Date.now();\n\t\t\t\t// If this is the first task to reach the global timeout, set the global delay\n\t\t\t\tif (!this.manager.globalDelay) {\n\t\t\t\t\t// The global delay function clears the global delay state when it is resolved\n\t\t\t\t\tthis.manager.globalDelay = this.globalDelayFor(timeout);\n\t\t\t\t}\n\t\t\t\tdelay = this.manager.globalDelay;\n\t\t\t} else {\n\t\t\t\t// Set RateLimitData based on the route-specific limit\n\t\t\t\tlimit = this.limit;\n\t\t\t\ttimeout = this.timeToReset;\n\t\t\t\tdelay = sleep(timeout, undefined, { ref: false });\n\t\t\t}\n\t\t\tconst rateLimitData: RateLimitData = {\n\t\t\t\ttimeToReset: timeout,\n\t\t\t\tlimit,\n\t\t\t\tmethod: options.method ?? 'get',\n\t\t\t\thash: this.hash,\n\t\t\t\turl,\n\t\t\t\troute: routeId.bucketRoute,\n\t\t\t\tmajorParameter: this.majorParameter,\n\t\t\t\tglobal: isGlobal,\n\t\t\t};\n\t\t\t// Let library users know they have hit a rate limit\n\t\t\tthis.manager.emit(RESTEvents.RateLimited, rateLimitData);\n\t\t\t// Determine whether a RateLimitError should be thrown\n\t\t\tawait this.onRateLimit(rateLimitData);\n\t\t\t// When not erroring, emit debug for what is happening\n\t\t\tif (isGlobal) {\n\t\t\t\tthis.debug(`Global rate limit hit, blocking all requests for ${timeout}ms`);\n\t\t\t} else {\n\t\t\t\tthis.debug(`Waiting ${timeout}ms for rate limit to pass`);\n\t\t\t}\n\t\t\t// Wait the remaining time left before the rate limit resets\n\t\t\tawait delay;\n\t\t}\n\t\t// As the request goes out, update the global usage information\n\t\tif (!this.manager.globalReset || this.manager.globalReset < Date.now()) {\n\t\t\tthis.manager.globalReset = Date.now() + 1000;\n\t\t\tthis.manager.globalRemaining = this.manager.options.globalRequestsPerSecond;\n\t\t}\n\t\tthis.manager.globalRemaining--;\n\n\t\tconst method = options.method ?? 'get';\n\n\t\tif (this.manager.listenerCount(RESTEvents.Request)) {\n\t\t\tthis.manager.emit(RESTEvents.Request, {\n\t\t\t\tmethod,\n\t\t\t\tpath: routeId.original,\n\t\t\t\troute: routeId.bucketRoute,\n\t\t\t\toptions,\n\t\t\t\tdata: bodyData,\n\t\t\t\tretries,\n\t\t\t});\n\t\t}\n\n\t\tconst controller = new AbortController();\n\t\tconst timeout = setTimeout(() => controller.abort(), this.manager.options.timeout).unref();\n\t\tlet res: Response;\n\n\t\ttry {\n\t\t\t// node-fetch typings are a bit weird, so we have to cast to any to get the correct signature\n\t\t\t// Type 'AbortSignal' is not assignable to type 'import(\"discord.js-modules/node_modules/@types/node-fetch/externals\").AbortSignal'\n\t\t\tres = await fetch(url, { ...options, signal: controller.signal as any });\n\t\t} catch (error: unknown) {\n\t\t\t// Retry the specified number of times for possible timed out requests\n\t\t\tif (error instanceof Error && error.name === 'AbortError' && retries !== this.manager.options.retries) {\n\t\t\t\treturn this.runRequest(routeId, url, options, bodyData, ++retries);\n\t\t\t}\n\n\t\t\tthrow error;\n\t\t} finally {\n\t\t\tclearTimeout(timeout);\n\t\t}\n\n\t\tif (this.manager.listenerCount(RESTEvents.Response)) {\n\t\t\tthis.manager.emit(\n\t\t\t\tRESTEvents.Response,\n\t\t\t\t{\n\t\t\t\t\tmethod,\n\t\t\t\t\tpath: routeId.original,\n\t\t\t\t\troute: routeId.bucketRoute,\n\t\t\t\t\toptions,\n\t\t\t\t\tdata: bodyData,\n\t\t\t\t\tretries,\n\t\t\t\t},\n\t\t\t\tres.clone(),\n\t\t\t);\n\t\t}\n\n\t\tlet retryAfter = 0;\n\n\t\tconst limit = res.headers.get('X-RateLimit-Limit');\n\t\tconst remaining = res.headers.get('X-RateLimit-Remaining');\n\t\tconst reset = res.headers.get('X-RateLimit-Reset-After');\n\t\tconst hash = res.headers.get('X-RateLimit-Bucket');\n\t\tconst retry = res.headers.get('Retry-After');\n\n\t\t// Update the total number of requests that can be made before the rate limit resets\n\t\tthis.limit = limit ? Number(limit) : Infinity;\n\t\t// Update the number of remaining requests that can be made before the rate limit resets\n\t\tthis.remaining = remaining ? Number(remaining) : 1;\n\t\t// Update the time when this rate limit resets (reset-after is in seconds)\n\t\tthis.reset = reset ? Number(reset) * 1000 + Date.now() + this.manager.options.offset : Date.now();\n\n\t\t// Amount of time in milliseconds until we should retry if rate limited (globally or otherwise)\n\t\tif (retry) retryAfter = Number(retry) * 1000 + this.manager.options.offset;\n\n\t\t// Handle buckets via the hash header retroactively\n\t\tif (hash && hash !== this.hash) {\n\t\t\t// Let library users know when rate limit buckets have been updated\n\t\t\tthis.debug(['Received bucket hash update', ` Old Hash : ${this.hash}`, ` New Hash : ${hash}`].join('\\n'));\n\t\t\t// This queue will eventually be eliminated via attrition\n\t\t\tthis.manager.hashes.set(`${method}:${routeId.bucketRoute}`, hash);\n\t\t}\n\n\t\t// Handle retryAfter, which means we have actually hit a rate limit\n\t\tlet sublimitTimeout: number | null = null;\n\t\tif (retryAfter > 0) {\n\t\t\tif (res.headers.get('X-RateLimit-Global')) {\n\t\t\t\tthis.manager.globalRemaining = 0;\n\t\t\t\tthis.manager.globalReset = Date.now() + retryAfter;\n\t\t\t} else if (!this.localLimited) {\n\t\t\t\t/*\n\t\t\t\t * This is a sublimit (e.g. 2 channel name changes/10 minutes) since the headers don't indicate a\n\t\t\t\t * route-wide rate limit. Don't update remaining or reset to avoid rate limiting the whole\n\t\t\t\t * endpoint, just set a reset time on the request itself to avoid retrying too soon.\n\t\t\t\t */\n\t\t\t\tsublimitTimeout = retryAfter;\n\t\t\t}\n\t\t}\n\n\t\t// Count the invalid requests\n\t\tif (res.status === 401 || res.status === 403 || res.status === 429) {\n\t\t\tif (!invalidCountResetTime || invalidCountResetTime < Date.now()) {\n\t\t\t\tinvalidCountResetTime = Date.now() + 1000 * 60 * 10;\n\t\t\t\tinvalidCount = 0;\n\t\t\t}\n\t\t\tinvalidCount++;\n\n\t\t\tconst emitInvalid =\n\t\t\t\tthis.manager.options.invalidRequestWarningInterval > 0 &&\n\t\t\t\tinvalidCount % this.manager.options.invalidRequestWarningInterval === 0;\n\t\t\tif (emitInvalid) {\n\t\t\t\t// Let library users know periodically about invalid requests\n\t\t\t\tthis.manager.emit(RESTEvents.InvalidRequestWarning, {\n\t\t\t\t\tcount: invalidCount,\n\t\t\t\t\tremainingTime: invalidCountResetTime - Date.now(),\n\t\t\t\t});\n\t\t\t}\n\t\t}\n\n\t\tif (res.ok) {\n\t\t\treturn parseResponse(res);\n\t\t} else if (res.status === 429) {\n\t\t\t// A rate limit was hit - this may happen if the route isn't associated with an official bucket hash yet, or when first globally rate limited\n\t\t\tconst isGlobal = this.globalLimited;\n\t\t\tlet limit: number;\n\t\t\tlet timeout: number;\n\n\t\t\tif (isGlobal) {\n\t\t\t\t// Set RateLimitData based on the global limit\n\t\t\t\tlimit = this.manager.options.globalRequestsPerSecond;\n\t\t\t\ttimeout = this.manager.globalReset + this.manager.options.offset - Date.now();\n\t\t\t} else {\n\t\t\t\t// Set RateLimitData based on the route-specific limit\n\t\t\t\tlimit = this.limit;\n\t\t\t\ttimeout = this.timeToReset;\n\t\t\t}\n\t\t\tawait this.onRateLimit({\n\t\t\t\ttimeToReset: timeout,\n\t\t\t\tlimit,\n\t\t\t\tmethod,\n\t\t\t\thash: this.hash,\n\t\t\t\turl,\n\t\t\t\troute: routeId.bucketRoute,\n\t\t\t\tmajorParameter: this.majorParameter,\n\t\t\t\tglobal: isGlobal,\n\t\t\t});\n\t\t\tthis.debug(\n\t\t\t\t[\n\t\t\t\t\t'Encountered unexpected 429 rate limit',\n\t\t\t\t\t` Global : ${isGlobal.toString()}`,\n\t\t\t\t\t` Method : ${method}`,\n\t\t\t\t\t` URL : ${url}`,\n\t\t\t\t\t` Bucket : ${routeId.bucketRoute}`,\n\t\t\t\t\t` Major parameter: ${routeId.majorParameter}`,\n\t\t\t\t\t` Hash : ${this.hash}`,\n\t\t\t\t\t` Limit : ${limit}`,\n\t\t\t\t\t` Retry After : ${retryAfter}ms`,\n\t\t\t\t\t` Sublimit : ${sublimitTimeout ? `${sublimitTimeout}ms` : 'None'}`,\n\t\t\t\t].join('\\n'),\n\t\t\t);\n\t\t\t// If caused by a sublimit, wait it out here so other requests on the route can be handled\n\t\t\tif (sublimitTimeout) {\n\t\t\t\t// Normally the sublimit queue will not exist, however, if a sublimit is hit while in the sublimit queue, it will\n\t\t\t\tconst firstSublimit = !this.#sublimitedQueue;\n\t\t\t\tif (firstSublimit) {\n\t\t\t\t\tthis.#sublimitedQueue = new AsyncQueue();\n\t\t\t\t\tvoid this.#sublimitedQueue.wait();\n\t\t\t\t\tthis.#asyncQueue.shift();\n\t\t\t\t}\n\t\t\t\tthis.#sublimitPromise?.resolve();\n\t\t\t\tthis.#sublimitPromise = null;\n\t\t\t\tawait sleep(sublimitTimeout, undefined, { ref: false });\n\t\t\t\tlet resolve: () => void;\n\t\t\t\tconst promise = new Promise<void>((res) => (resolve = res));\n\t\t\t\tthis.#sublimitPromise = { promise, resolve: resolve! };\n\t\t\t\tif (firstSublimit) {\n\t\t\t\t\t// Re-queue this request so it can be shifted by the finally\n\t\t\t\t\tawait this.#asyncQueue.wait();\n\t\t\t\t\tthis.#shiftSublimit = true;\n\t\t\t\t}\n\t\t\t}\n\t\t\t// Since this is not a server side issue, the next request should pass, so we don't bump the retries counter\n\t\t\treturn this.runRequest(routeId, url, options, bodyData, retries);\n\t\t} else if (res.status >= 500 && res.status < 600) {\n\t\t\t// Retry the specified number of times for possible server side issues\n\t\t\tif (retries !== this.manager.options.retries) {\n\t\t\t\treturn this.runRequest(routeId, url, options, bodyData, ++retries);\n\t\t\t}\n\t\t\t// We are out of retries, throw an error\n\t\t\tthrow new HTTPError(res.statusText, res.constructor.name, res.status, method, url, bodyData);\n\t\t} else {\n\t\t\t// Handle possible malformed requests\n\t\t\tif (res.status >= 400 && res.status < 500) {\n\t\t\t\t// If we receive this status code, it means the token we had is no longer valid.\n\t\t\t\tif (res.status === 401) {\n\t\t\t\t\tthis.manager.setToken(null!);\n\t\t\t\t}\n\t\t\t\t// The request will not succeed for some reason, parse the error returned from the api\n\t\t\t\tconst data = (await parseResponse(res)) as DiscordErrorData;\n\t\t\t\t// throw the API error\n\t\t\t\tthrow new DiscordAPIError(data, data.code, res.status, method, url, bodyData);\n\t\t\t}\n\t\t\treturn null;\n\t\t}\n\t}\n}\n", "import type { RESTPatchAPIChannelJSONBody } from 'discord-api-types/v9';\nimport type { Response } from 'node-fetch';\nimport { RequestMethod } from '../RequestManager';\n\n/**\n * Converts the response to usable data\n * @param res The node-fetch response\n */\nexport function parseResponse(res: Response): Promise<unknown> {\n\tif (res.headers.get('Content-Type')?.startsWith('application/json')) {\n\t\treturn res.json();\n\t}\n\n\treturn res.buffer();\n}\n\n/**\n * Check whether a request falls under a sublimit\n * @param bucketRoute The buckets route identifier\n * @param body The options provided as JSON data\n * @param method The HTTP method that will be used to make the request\n * @returns Whether the request falls under a sublimit\n */\nexport function hasSublimit(bucketRoute: string, body?: unknown, method?: string): boolean {\n\t// TODO: Update for new sublimits\n\t// Currently known sublimits:\n\t// Editing channel `name` or `topic`\n\tif (bucketRoute === '/channels/:id') {\n\t\tif (typeof body !== 'object' || body === null) return false;\n\t\t// This should never be a POST body, but just in case\n\t\tif (method !== RequestMethod.Patch) return false;\n\t\tconst castedBody = body as RESTPatchAPIChannelJSONBody;\n\t\treturn ['name', 'topic'].some((key) => Reflect.has(castedBody, key));\n\t}\n\n\treturn false;\n}\n", "import { EventEmitter } from 'node:events';\nimport { CDN } from './CDN';\nimport { InternalRequest, RequestData, RequestManager, RequestMethod, RouteLike } from './RequestManager';\nimport { DefaultRestOptions, RESTEvents } from './utils/constants';\nimport type { AgentOptions } from 'node:https';\nimport type { RequestInit, Response } from 'node-fetch';\n\n/**\n * Options to be passed when creating the REST instance\n */\nexport interface RESTOptions {\n\t/**\n\t * HTTPS Agent options\n\t * @default {}\n\t */\n\tagent: Omit<AgentOptions, 'keepAlive'>;\n\t/**\n\t * The base api path, without version\n\t * @default 'https://discord.com/api'\n\t */\n\tapi: string;\n\t/**\n\t * The cdn path\n\t * @default 'https://cdn.discordapp.com'\n\t */\n\tcdn: string;\n\t/**\n\t * Additional headers to send for all API requests\n\t * @default {}\n\t */\n\theaders: Record<string, string>;\n\t/**\n\t * 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).\n\t * That is, if set to 500, warnings will be emitted at invalid request number 500, 1000, 1500, and so on.\n\t * @default 0\n\t */\n\tinvalidRequestWarningInterval: number;\n\t/**\n\t * How many requests to allow sending per second (Infinity for unlimited, 50 for the standard global limit used by Discord)\n\t * @default 50\n\t */\n\tglobalRequestsPerSecond: number;\n\t/**\n\t * The extra offset to add to rate limits in milliseconds\n\t * @default 50\n\t */\n\toffset: number;\n\t/**\n\t * Determines how rate limiting and pre-emptive throttling should be handled.\n\t * When an array of strings, each element is treated as a prefix for the request route\n\t * (e.g. `/channels` to match any route starting with `/channels` such as `/channels/:id/messages`)\n\t * for which to throw {@link RateLimitError}s. All other request routes will be queued normally\n\t * @default null\n\t */\n\trejectOnRateLimit: string[] | RateLimitQueueFilter | null;\n\t/**\n\t * The number of retries for errors with the 500 code, or errors\n\t * that timeout\n\t * @default 3\n\t */\n\tretries: number;\n\t/**\n\t * The time to wait in milliseconds before a request is aborted\n\t * @default 15_000\n\t */\n\ttimeout: number;\n\t/**\n\t * Extra information to add to the user agent\n\t * @default `Node.js ${process.version}`\n\t */\n\tuserAgentAppendix: string;\n\t/**\n\t * The version of the API to use\n\t * @default '9'\n\t */\n\tversion: string;\n}\n\n/**\n * Data emitted on `RESTEvents.RateLimited`\n */\nexport interface RateLimitData {\n\t/**\n\t * The time, in milliseconds, until the request-lock is reset\n\t */\n\ttimeToReset: number;\n\t/**\n\t * The amount of requests we can perform before locking requests\n\t */\n\tlimit: number;\n\t/**\n\t * The HTTP method being performed\n\t */\n\tmethod: string;\n\t/**\n\t * The bucket hash for this request\n\t */\n\thash: string;\n\t/**\n\t * The full URL for this request\n\t */\n\turl: string;\n\t/**\n\t * The route being hit in this request\n\t */\n\troute: string;\n\t/**\n\t * The major parameter of the route\n\t *\n\t * For example, in `/channels/x`, this will be `x`.\n\t * If there is no major parameter (e.g: `/bot/gateway`) this will be `global`.\n\t */\n\tmajorParameter: string;\n\t/**\n\t * Whether the rate limit that was reached