import { EventEmitter } from 'node:events'; import { CDN } from './CDN'; import { InternalRequest, RequestData, RequestManager, RequestMethod, RouteLike } from './RequestManager'; import { DefaultRestOptions, RESTEvents } from './utils/constants'; import type { AgentOptions } from 'node:https'; import type { RequestInit, Response } from 'node-fetch'; /** * Options to be passed when creating the REST instance */ export interface RESTOptions { /** * HTTPS Agent options * @default {} */ agent: Omit; /** * The base api path, without version * @default 'https://discord.com/api' */ api: string; /** * The cdn path * @default 'https://cdn.discordapp.com' */ cdn: string; /** * Additional headers to send for all API requests * @default {} */ headers: Record; /** * 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. * @default 0 */ invalidRequestWarningInterval: number; /** * How many requests to allow sending per second (Infinity for unlimited, 50 for the standard global limit used by Discord) * @default 50 */ globalRequestsPerSecond: number; /** * The extra offset to add to rate limits in milliseconds * @default 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 * @default null */ rejectOnRateLimit: string[] | RateLimitQueueFilter | null; /** * The number of retries for errors with the 500 code, or errors * that timeout * @default 3 */ retries: number; /** * The time to wait in milliseconds before a request is aborted * @default 15_000 */ timeout: number; /** * Extra information to add to the user agent * @default `Node.js ${process.version}` */ userAgentAppendix: string; /** * The version of the API to use * @default '9' */ version: string; } /** * Data emitted on `RESTEvents.RateLimited` */ export interface RateLimitData { /** * The time, in milliseconds, until the request-lock is reset */ timeToReset: number; /** * The amount of requests we can perform before locking requests */ limit: number; /** * The HTTP method being performed */ method: string; /** * The bucket hash for this request */ hash: string; /** * The full URL for this request */ url: string; /** * The route being hit in this request */ route: string; /** * 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; /** * Whether the rate limit that was reached was the global limit */ global: boolean; } /** * A function that determines whether the rate limit hit should throw an Error */ export type RateLimitQueueFilter = (rateLimitData: RateLimitData) => boolean | Promise; export interface APIRequest { /** * The HTTP method used in this request */ method: string; /** * The full path used to make the request */ path: RouteLike; /** * The API route identifying the ratelimit for this request */ route: string; /** * Additional HTTP options for this request */ options: RequestInit; /** * The data that was used to form the body of this request */ data: Pick; /** * The number of times this request has been attempted */ retries: number; } export interface InvalidRequestWarningData { /** * Number of invalid requests that have been made in the window */ count: number; /** * Time in ms remaining before the count resets */ remainingTime: number; } export interface RestEvents { invalidRequestWarning: [invalidRequestInfo: InvalidRequestWarningData]; restDebug: [info: string]; rateLimited: [rateLimitInfo: RateLimitData]; request: [request: APIRequest]; response: [request: APIRequest, response: Response]; newListener: [name: string, listener: (...args: any) => void]; removeListener: [name: string, listener: (...args: any) => void]; } export interface REST { on: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & ((event: Exclude, listener: (...args: any[]) => void) => this); once: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & ((event: Exclude, listener: (...args: any[]) => void) => this); emit: ((event: K, ...args: RestEvents[K]) => boolean) & ((event: Exclude, ...args: any[]) => boolean); off: ((event: K, listener: (...args: RestEvents[K]) => void) => this) & ((event: Exclude, listener: (...args: any[]) => void) => this); removeAllListeners: ((event?: K) => this) & ((event?: Exclude) => this); } export class REST extends EventEmitter { public readonly cdn: CDN; public readonly requestManager: RequestManager; public constructor(options: Partial = {}) { super(); this.cdn = new CDN(options.cdn ?? DefaultRestOptions.cdn); this.requestManager = new RequestManager(options) .on(RESTEvents.Debug, this.emit.bind(this, RESTEvents.Debug)) .on(RESTEvents.RateLimited, this.emit.bind(this, RESTEvents.RateLimited)) .on(RESTEvents.InvalidRequestWarning, this.emit.bind(this, RESTEvents.InvalidRequestWarning)); this.on('newListener', (name, listener) => { if (name === RESTEvents.Request || name === RESTEvents.Response) this.requestManager.on(name, listener); }); this.on('removeListener', (name, listener) => { if (name === RESTEvents.Request || name === RESTEvents.Response) this.requestManager.off(name, listener); }); } /** * Sets the authorization token that should be used for requests * @param token The authorization token to use */ public setToken(token: string) { 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 */ public get(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Get }); } /** * Runs a delete request from the api * @param fullRoute The full route to query * @param options Optional request options */ public delete(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Delete }); } /** * Runs a post request from the api * @param fullRoute The full route to query * @param options Optional request options */ public post(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Post }); } /** * Runs a put request from the api * @param fullRoute The full route to query * @param options Optional request options */ public put(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Put }); } /** * Runs a patch request from the api * @param fullRoute The full route to query * @param options Optional request options */ public patch(fullRoute: RouteLike, options: RequestData = {}) { return this.request({ ...options, fullRoute, method: RequestMethod.Patch }); } /** * Runs a request from the api * @param options Request options */ public request(options: InternalRequest) { return this.requestManager.queueRequest(options); } }