diff --git a/package-lock.json b/package-lock.json index 77fa56fee9b4..87a18f9201d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,6 +12,7 @@ "@discordjs/collection": "^0.2.1", "@discordjs/form-data": "^3.0.1", "@sapphire/async-queue": "^1.1.5", + "@types/node-fetch": "^2.5.12", "@types/ws": "^8.2.0", "discord-api-types": "^0.23.1", "node-fetch": "^2.6.1", @@ -2307,6 +2308,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz", "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==" }, + "node_modules/@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "node_modules/@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -5636,7 +5646,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", @@ -14018,6 +14027,15 @@ "resolved": "https://registry.npmjs.org/@types/node/-/node-16.10.2.tgz", "integrity": "sha512-zCclL4/rx+W5SQTzFs9wyvvyCwoK9QtBpratqz2IYJ3O8Umrn0m3nsTv0wQBk9sRGpvUe9CwPDrQFB10f1FIjQ==" }, + "@types/node-fetch": { + "version": "2.5.12", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.12.tgz", + "integrity": "sha512-MKgC4dlq4kKNa/mYrwpKfzQMB5X3ee5U6fSprkKpToBqBmX4nFZL9cW5jl6sWn+xpRJ7ypWh2yyqqr8UUCstSw==", + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, "@types/normalize-package-data": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", @@ -16627,7 +16645,6 @@ "version": "3.0.1", "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, "requires": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", diff --git a/package.json b/package.json index dae2659dc591..f35cdfa5194f 100644 --- a/package.json +++ b/package.json @@ -52,6 +52,7 @@ "@discordjs/collection": "^0.2.1", "@discordjs/form-data": "^3.0.1", "@sapphire/async-queue": "^1.1.5", + "@types/node-fetch": "^2.5.12", "@types/ws": "^8.2.0", "discord-api-types": "^0.23.1", "node-fetch": "^2.6.1", diff --git a/src/rest/RequestHandler.js b/src/rest/RequestHandler.js index d80570dac224..6ad8fdfa703a 100644 --- a/src/rest/RequestHandler.js +++ b/src/rest/RequestHandler.js @@ -5,7 +5,7 @@ const DiscordAPIError = require('./DiscordAPIError'); const HTTPError = require('./HTTPError'); const RateLimitError = require('./RateLimitError'); const { - Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING }, + Events: { DEBUG, RATE_LIMIT, INVALID_REQUEST_WARNING, API_RESPONSE, API_REQUEST }, } = require('../util/Constants'); const Util = require('../util/Util'); @@ -162,6 +162,34 @@ class RequestHandler { } this.manager.globalRemaining--; + /** + * Represents a request that will or has been made to the Discord API + * @typedef {Object} APIRequest + * @property {HTTPMethod} method The HTTP method used in this request + * @property {string} path The full path used to make the request + * @property {string} route The API route identifying the ratelimit for this request + * @property {Object} options Additional options for this request + * @property {number} retries The number of times this request has been attempted + */ + + if (this.manager.client.listenerCount(API_REQUEST)) { + /** + * Emitted before every API request. + * This event can emit several times for the same request, e.g. when hitting a rate limit. + * This is an informational event that is emitted quite frequently, + * it is highly recommended to check `request.path` to filter the data. + * @event Client#apiRequest + * @param {APIRequest} request The request that is about to be sent + */ + this.manager.client.emit(API_REQUEST, { + method: request.method, + path: request.path, + route: request.route, + options: request.options, + retries: request.retries, + }); + } + // Perform the request let res; try { @@ -176,6 +204,29 @@ class RequestHandler { return this.execute(request); } + if (this.manager.client.listenerCount(API_RESPONSE)) { + /** + * Emitted after every API request has received a response. + * This event does not necessarily correlate to completion of the request, e.g. when hitting a rate limit. + * This is an informational event that is emitted quite frequently, + * it is highly recommended to check `request.path` to filter the data. + * @event Client#apiResponse + * @param {APIRequest} request The request that triggered this response + * @param {Response} response The response received from the Discord API + */ + this.manager.client.emit( + API_RESPONSE, + { + method: request.method, + path: request.path, + route: request.route, + options: request.options, + retries: request.retries, + }, + res.clone(), + ); + } + let sublimitTimeout; if (res.headers) { const serverDate = res.headers.get('date'); @@ -315,3 +366,13 @@ class RequestHandler { } module.exports = RequestHandler; + +/** + * @external HTTPMethod + * @see {@link https://developer.mozilla.org/en-US/docs/Web/HTTP/Methods} + */ + +/** + * @external Response + * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Response} + */ diff --git a/src/util/Constants.js b/src/util/Constants.js index b6e488b796e9..82c8c5fa3765 100644 --- a/src/util/Constants.js +++ b/src/util/Constants.js @@ -124,6 +124,8 @@ exports.Opcodes = { exports.Events = { RATE_LIMIT: 'rateLimit', INVALID_REQUEST_WARNING: 'invalidRequestWarning', + API_RESPONSE: 'apiResponse', + API_REQUEST: 'apiRequest', CLIENT_READY: 'ready', APPLICATION_COMMAND_CREATE: 'applicationCommandCreate', APPLICATION_COMMAND_DELETE: 'applicationCommandDelete', diff --git a/typings/index.d.ts b/typings/index.d.ts index 7d8c60a0ae76..4816402f9216 100644 --- a/typings/index.d.ts +++ b/typings/index.d.ts @@ -49,6 +49,7 @@ import { import { ChildProcess } from 'node:child_process'; import { EventEmitter } from 'node:events'; import { AgentOptions } from 'node:https'; +import { Response } from 'node-fetch'; import { Stream } from 'node:stream'; import { MessagePort, Worker } from 'node:worker_threads'; import * as WebSocket from 'ws'; @@ -3129,6 +3130,14 @@ export interface APIErrors { STICKER_ANIMATION_DURATION_EXCEEDS_MAXIMUM_OF_5_SECONDS: 170007; } +export interface APIRequest { + method: 'get' | 'post' | 'delete' | 'patch' | 'put'; + options: unknown; + path: string; + retries: number; + route: string; +} + export interface ApplicationAsset { name: string; id: Snowflake; @@ -3439,6 +3448,8 @@ export interface ChannelWebhookCreateOptions { } export interface ClientEvents { + apiResponse: [request: APIRequest, response: Response]; + apiRequest: [request: APIRequest]; applicationCommandCreate: [command: ApplicationCommand]; applicationCommandDelete: [command: ApplicationCommand]; applicationCommandUpdate: [oldCommand: ApplicationCommand | null, newCommand: ApplicationCommand]; @@ -3677,6 +3688,8 @@ export interface ConstantsColors { export interface ConstantsEvents { RATE_LIMIT: 'rateLimit'; INVALID_REQUEST_WARNING: 'invalidRequestWarning'; + API_RESPONSE: 'apiResponse'; + API_REQUEST: 'apiRequest'; CLIENT_READY: 'ready'; APPLICATION_COMMAND_CREATE: 'applicationCommandCreate'; APPLICATION_COMMAND_DELETE: 'applicationCommandDelete';